diff --git a/.changeset/green-spies-arrive.md b/.changeset/green-spies-arrive.md deleted file mode 100644 index 76575bd6ef63..000000000000 --- a/.changeset/green-spies-arrive.md +++ /dev/null @@ -1,29 +0,0 @@ ---- -"fluid-framework": minor -"@fluidframework/tree": minor ---- ---- -section: tree -highlight: true ---- - -✨ New! `Record`-typed objects can now be used to construct MapNodes - -You can now construct MapNodes from `Record` typed objects, similar to how maps are expressed in JSON. - -Before this change, an `Iterable` was required, but now an object like `{key1: Child1, key2: Child2}` is allowed. - -Full example using this new API: - -```typescript -class Schema extends schemaFactory.map("ExampleMap", schemaFactory.number) {} -const fromRecord = new Schema({ x: 5 }); -``` - -This new feature makes it possible for schemas to construct a tree entirely from JSON-compatible objects using their constructors, -as long as they do not require unhydrated nodes to differentiate ambiguous unions, -or IFluidHandles (which themselves are not JSON compatible). - -Due to limitations of TypeScript and recursive types, -recursive maps do not advertise support for this feature in their typing, -but it works at runtime. diff --git a/.changeset/light-bags-wash.md b/.changeset/light-bags-wash.md deleted file mode 100644 index 241708fcde13..000000000000 --- a/.changeset/light-bags-wash.md +++ /dev/null @@ -1,30 +0,0 @@ ---- -"fluid-framework": minor -"@fluidframework/tree": minor ---- ---- -section: tree ---- - -Add `ITreeConfigurationOptions.preventAmbiguity` - -The new `ITreeConfigurationOptions.preventAmbiguity` flag can be set to true to enable checking of some additional rules when constructing the `TreeViewConfiguration`. - -This example shows an ambiguous schema: - -```typescript -const schemaFactory = new SchemaFactory("com.example"); -class Feet extends schemaFactory.object("Feet", { length: schemaFactory.number }) {} -class Meters extends schemaFactory.object("Meters", { length: schemaFactory.number }) {} -const config = new TreeViewConfiguration({ - // This combination of schema can lead to ambiguous cases, and will error since preventAmbiguity is true. - schema: [Feet, Meters], - preventAmbiguity: true, -}); -const view = tree.viewWith(config); -// This is invalid since it is ambiguous which type of node is being constructed. -// The error thrown above when constructing the TreeViewConfiguration is because of this ambiguous case: -view.initialize({ length: 5 }); -``` - -See the documentation on `ITreeConfigurationOptions.preventAmbiguity` for a more complete example and more details. diff --git a/.changeset/many-moments-juggle.md b/.changeset/many-moments-juggle.md deleted file mode 100644 index 3380f983be15..000000000000 --- a/.changeset/many-moments-juggle.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -"@fluidframework/container-loader": minor ---- ---- -section: deprecation ---- - -container-loader: summarizeProtocolTree and its corresponding duplicate ILoaderOptions definition is deprecated - -The `summarizeProtocolTree` property in ILoaderOptions was added to test single-commit summaries during the initial -implementation phase. The flag is no longer required and should no longer be used, and is now marked deprecated. If a -driver needs to enable or disable single-commit summaries, it can do so via `IDocumentServicePolicies`. diff --git a/.changeset/moody-meals-wonder.md b/.changeset/moody-meals-wonder.md deleted file mode 100644 index 9338b7258301..000000000000 --- a/.changeset/moody-meals-wonder.md +++ /dev/null @@ -1,31 +0,0 @@ ---- -"fluid-framework": minor -"@fluidframework/tree": minor ---- - -Workaround issue where recursive tree schemas using MapNodes produce invalid d.ts files. - -```typescript -export class RecursiveMap extends schema.mapRecursive("RM", [() => RecursiveMap]) {} -{ - type _check = ValidateRecursiveSchema; -} -``` - -A recursive map schema like the above (which follows all our recommended best practices for maximum chances of working) would work when used from within its compilation unit, but would generate d.ts that fails to compile when exporting it: - -```typescript -declare const RecursiveMap_base: import("@fluidframework/tree").TreeNodeSchemaClass<"com.example.RM", import("@fluidframework/tree").NodeKind.Map, import("@fluidframework/tree").TreeMapNodeUnsafe typeof RecursiveMap]> & import("@fluidframework/tree").WithType<"com.example.RM">, { - [Symbol.iterator](): Iterator<[string, RecursiveMap], any, undefined>; -}, false, readonly [() => typeof RecursiveMap]>; -export declare class RecursiveMap extends RecursiveMap_base { -} -``` - -This results in the compile error: - -> error TS2310: Type 'RecursiveMap' recursively references itself as a base type. - -This is in TypeScript 5.4.5. - -With this change, that error is fixed by modifying the `TreeMapNodeUnsafe` type it references to inline the definition of `ReadonlyMap` instead of using the one from the TypeScript standard library. diff --git a/.changeset/moody-toys-dance.md b/.changeset/moody-toys-dance.md deleted file mode 100644 index ff1e22d0e731..000000000000 --- a/.changeset/moody-toys-dance.md +++ /dev/null @@ -1,35 +0,0 @@ ---- -"@fluidframework/tree": minor -"fluid-framework": minor ---- ---- -section: tree -highlight: true ---- - -✨ New! When unambiguous, ArrayNodes can now be constructed from Maps and MapNodes from arrays - -Since the types for ArrayNodes and MapNodes indicate they can be constructed from iterables, -it should work, even if those iterables are themselves arrays or maps. -To avoid this being a breaking change, a priority system was introduced. -ArrayNodes will only be implicitly constructable from JavaScript Map objects in contexts where no MapNodes are allowed. -Similarly MapNodes will only be implicitly constructable from JavaScript Array objects in contexts where no ArrayNodes are allowed. - -In practice, the main case in which this is likely to matter is when implicitly constructing a map node. If you provide an array of key value pairs, this now works instead of erroring, as long as no ArrayNode is valid at that location in the tree. - -```typescript -class MyMapNode extends schemaFactory.map("x", schemaFactory.number) {} -class Root extends schemaFactory.object("root", { data: MyMapNode }) {} -// This now works (before it compiled, but error at runtime): -const fromArray = new Root({ data: [["x", 5]] }); -``` - -Prior versions used to have to do: -```typescript -new Root({ data: new MyMapNode([["x", 5]]) }); -``` -or: -```typescript -new Root({ data: new Map([["x", 5]]) }); -``` -Both of these options still work: strictly more cases are allowed with this change. diff --git a/.changeset/old-teeth-study.md b/.changeset/old-teeth-study.md deleted file mode 100644 index 609aff16a235..000000000000 --- a/.changeset/old-teeth-study.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -"@fluid-experimental/property-shared-tree-interop": minor ---- ---- -section: other ---- -Remove PropertyDDS/SharedTree Schema Converter - -This schema converter had several known issues and has been removed. Read the [schema converter section](https://github.com/microsoft/FluidFramework/blob/main/experimental/PropertyDDS/packages/property-shared-tree-interop/README.md#schema-converter-runtime) of the package readme for more details. diff --git a/.changeset/olive-areas-bow.md b/.changeset/olive-areas-bow.md deleted file mode 100644 index a723e3c106a9..000000000000 --- a/.changeset/olive-areas-bow.md +++ /dev/null @@ -1,18 +0,0 @@ ---- -"fluid-framework": minor -"@fluidframework/tree": minor ---- ---- -section: tree ---- - -Implicit TreeNode construction improvements - -ArrayNodes and MapNodes could always be explicitly constructed (using `new`) from iterables. -The types also allowed using of iterables to construct implicitly construct array nodes and map nodes, -but this did not work at runtime. -This has been fixed for all cases except implicitly constructing an ArrayNode form an `Iterable` that is actually a `Map`, -and implicitly constructing a MapNode from an `Iterable` that is actually an `Array`. -These cases may be fixed in the future, but require additional work to ensure unions of array nodes and map nodes work correctly. - -Additionally MapNodes can now be constructed from `Iterator` where previously the inner arrays had to be mutable. diff --git a/.changeset/orange-ads-join.md b/.changeset/orange-ads-join.md deleted file mode 100644 index 98d9bba74ffe..000000000000 --- a/.changeset/orange-ads-join.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -"@fluidframework/container-runtime": minor -"@fluidframework/runtime-definitions": minor ---- - -These properties `gcThrowOnTombstoneUsage` and `gcTombstoneEnforcementAllowed` have been deprecated in `IFluidParentContext` and `ContainerRuntime`. These were included in certain garbage collection telemetry to identify whether the corresponding features have been enabled. These features are now enabled by default and this information is added to the "GarbageCollectorLoaded" telemetry. - -Also, the following Garbage collection runtime options and configs have been removed. They were added during GC feature development to roll out and control functionalities. The functionalities corresponding are on by default and can no longer be controlled: - -GC runtime options removed: -- `gcDisableThrowOnTombstoneLoad` -- `disableDataStoreSweep` - -GC configs removed: -- `"Fluid.GarbageCollection.DisableTombstone"` -- `"Fluid.GarbageCollection.ThrowOnTombstoneUsage"` -- `"Fluid.GarbageCollection.DisableDataStoreSweep"` diff --git a/.changeset/ready-windows-switch.md b/.changeset/ready-windows-switch.md deleted file mode 100644 index 29052a58f54f..000000000000 --- a/.changeset/ready-windows-switch.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -"@fluidframework/container-runtime": minor ---- ---- -section: deprecation ---- - -InactiveResponseHeaderKey header is deprecated - -The header `InactiveResponseHeaderKey` is deprecated and will be removed in the future. It was part of an experimental feature where loading an inactive data store would result in returning a 404 with this header set to true. This feature is no longer supported. diff --git a/.changeset/silver-rivers-judge.md b/.changeset/silver-rivers-judge.md deleted file mode 100644 index fbb98b7bab44..000000000000 --- a/.changeset/silver-rivers-judge.md +++ /dev/null @@ -1,44 +0,0 @@ ---- -"fluid-framework": minor -"@fluidframework/tree": minor ---- ---- -section: tree ---- - -Enforce use of TreeViewConfiguration's constructor - -`TreeViewConfiguration` is `@sealed`, meaning creating custom implementations of it such as assigning object literals to a `TreeViewConfiguration` or sub-classing it are not supported. -This reserved the ability for the Fluid Framework to add members to this class over time, informing users that they must use it in such a way where such changes are non-breaking. -However, there was no compiler-based enforcement of this expectation. -It was only indicated via documentation and an implicit assumption that when an API takes in a typed defined as a class, that an instance of that class must be used rather than an arbitrary object of a similar shape. - -With this change, the TypeScript compiler will now inform users when they invalidly provide an object literal as a `TreeViewConfiguration`. - -More specifically this causes code like this to produce a compile error: - -```typescript -// Don't do this! -const view = tree.viewWith( - { schema: TestNode, enableSchemaValidation: false }, -); -``` - -The above was never intended to work, and is not a supported use of the `viewWith` since it requires a `TreeViewConfiguration` which is sealed. -Any code using the above pattern will break in Fluid Framework 2.2 and above. Such code will need to be updated to the pattern shown below. -Any code broken by this change is technically unsupported and only worked due to a gap in the type checking. This is not considered a breaking change. -The correct way to get a `TreeViewConfiguration` is by using its constructor: - -```typescript -// This pattern correctly initializes default values and validates input. -const view = tree.viewWith( - new TreeViewConfiguration({ schema: TestNode }), -); -``` - -Skipping the constructor causes the following problems: - -1. `TreeViewConfiguration` does validation in its constructor, so skipping it also skips the validation which leads to much less friendly error messages for invalid schema. -2. Skipping the constructor also discards any default values for options like `enableSchemaValidation`. -This means that code written in that style would break if more options were added. Since such changes are planned, -it is not practical to support this pattern. diff --git a/.changeset/smart-toys-repeat.md b/.changeset/smart-toys-repeat.md deleted file mode 100644 index 0fb62f8169f1..000000000000 --- a/.changeset/smart-toys-repeat.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -"fluid-framework": minor -"@fluidframework/runtime-utils": minor ---- ---- -section: feature ---- - -New `isFluidHandle` type guard to check if an object is an `IFluidHandle` - -The `isFluidHandle` type guard function is now exported and can be used to detect which objects are `IFluidHandle`s. -Since `IFluidHandle` often needs special handling (for example when serializing since it's not JSON compatible), -having a dedicated detection function for it is useful. -Doing this detection was possible previously using the `tree` package's schema system via `Tree.is(value, new SchemaFactory("").handle)`, -but can now be done with just `isFluidHandle(value)`. diff --git a/.changeset/smooth-yaks-unite.md b/.changeset/smooth-yaks-unite.md new file mode 100644 index 000000000000..8a33a6c0c441 --- /dev/null +++ b/.changeset/smooth-yaks-unite.md @@ -0,0 +1,13 @@ +--- +"@fluidframework/tree": minor +--- +--- +"section": "tree" +--- +Refactor code for emitting events to make it easier to copy paste into other projects. + +Factored event emitting utilities into their own file, `events/emitter.ts`. +Applications wishing to use SharedTree's eventing library for custom events can copy this file (and its referenced utility function) as a starting point for defining and emitting their own custom events. +See `createEmitter`'s documentation for example usage. + +Currently there are no published or officially supported versions of these utilities, but they are relatively simple, and can be copied and customized as needed. diff --git a/.changeset/strong-mice-talk.md b/.changeset/strong-mice-talk.md deleted file mode 100644 index 0df0b64ac046..000000000000 --- a/.changeset/strong-mice-talk.md +++ /dev/null @@ -1,8 +0,0 @@ ---- -"fluid-framework": minor -"@fluidframework/tree": minor ---- - -Add a function `isRepoSuperset` to determine if changes to a document schema are backward-compatible. - -Note: These changes are not customer-facing and make progress toward future plans in Tree's schema evolution space. diff --git a/.changeset/sweet-things-wash.md b/.changeset/sweet-things-wash.md deleted file mode 100644 index 43bdabb8031a..000000000000 --- a/.changeset/sweet-things-wash.md +++ /dev/null @@ -1,27 +0,0 @@ ---- -"@fluidframework/tree": minor ---- - -Add `@alpha` API `FixRecursiveArraySchema` as a workaround around an issue with recursive ArrayNode schema. - -Importing a recursive ArrayNode schema via a d.ts file can produce an error like -`error TS2310: Type 'RecursiveArray' recursively references itself as a base type.` -if using a tsconfig with `"skipLibCheck": false`. - -This error occurs due to the TypeScript compiler splitting the class definition into two separate declarations in the d.ts file (one for the base, and one for the actual class). -For unknown reasons, splitting the class declaration in this way breaks the recursive type handling, leading to the mentioned error. - -Since recursive type handling in TypeScript is order dependent, putting just the right kind of usages of the type before the declarations can cause it to not hit this error. -For the case of ArrayNodes, this can be done via usage that looks like this: - -```typescript -/** - * Workaround to avoid - * `error TS2310: Type 'RecursiveArray' recursively references itself as a base type.` in the d.ts file. - */ -export declare const _RecursiveArrayWorkaround: FixRecursiveArraySchema; -export class RecursiveArray extends schema.arrayRecursive("RA", [() => RecursiveArray]) {} -{ - type _check = ValidateRecursiveSchema; -} -``` diff --git a/.changeset/thick-birds-vanish.md b/.changeset/thick-birds-vanish.md deleted file mode 100644 index d7530e3accd6..000000000000 --- a/.changeset/thick-birds-vanish.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -"@fluidframework/tree": minor ---- ---- -section: tree ---- - -Fix document-corrupting bug when rebasing over move compositions. - -Before this fix, if multiple users concurrently performed moves (possibly by reverting prior moves), there was a chance that the document would become corrupted. diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index cc9bc0efbc6e..4c3bb89b3d9a 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -50,10 +50,12 @@ # Fluid Devtools source /packages/tools/devtools/**/src @microsoft/fluid-cr-devtools -# Non-internal API changes -/**/api-report/*.public.api.md @microsoft/fluid-cr-api -/**/api-report/*.beta.api.md @microsoft/fluid-cr-api -/**/api-report/*.alpha.api.md @microsoft/fluid-cr-api +# API report changes +# TODO: if we ever add `.internal` API reports, we will want to omit them here +/**/api-report/*.api.md @microsoft/fluid-cr-api +/build-tools/**/api-report/*.api.md # Do not require API review for build-tools packages +/server/**/api-report/*.api.md # Do not require API review for server packages +/tools/**/api-report/*.api.md # Do not require API review for tools packages # Changesets and release notes /**/.changeset/*.md @microsoft/fluid-cr-docs diff --git a/.github/workflows/changeset-reporter.yml b/.github/workflows/changeset-reporter.yml index 04fb275d243b..b7ee72c5bf99 100644 --- a/.github/workflows/changeset-reporter.yml +++ b/.github/workflows/changeset-reporter.yml @@ -35,7 +35,8 @@ jobs: - name: Required but missing if: fromJson(steps.changeset.outputs.CHANGESET).required == true && fromJson(steps.changeset.outputs.CHANGESET).changesetFound == false - uses: marocchino/sticky-pull-request-comment@fcf6fe9e4a0409cd9316a5011435be0f3327f1e1 # ratchet:marocchino/sticky-pull-request-comment@v2.3.1 + # release notes: https://github.com/marocchino/sticky-pull-request-comment/releases/tag/v2.9.0 + uses: marocchino/sticky-pull-request-comment@331f8f5b4215f0445d3c07b4967662a32a2d3e31 # ratchet:marocchino/sticky-pull-request-comment@v2.9.0 with: header: changeset number: ${{ fromJson(steps.changeset.outputs.CHANGESET).pr }} @@ -43,7 +44,8 @@ jobs: - name: Required and present if: fromJson(steps.changeset.outputs.CHANGESET).required == true && fromJson(steps.changeset.outputs.CHANGESET).changesetFound == true - uses: marocchino/sticky-pull-request-comment@fcf6fe9e4a0409cd9316a5011435be0f3327f1e1 # ratchet:marocchino/sticky-pull-request-comment@v2.3.1 + # release notes: https://github.com/marocchino/sticky-pull-request-comment/releases/tag/v2.9.0 + uses: marocchino/sticky-pull-request-comment@331f8f5b4215f0445d3c07b4967662a32a2d3e31 # ratchet:marocchino/sticky-pull-request-comment@v2.9.0 with: header: changeset number: ${{ fromJson(steps.changeset.outputs.CHANGESET).pr }} @@ -53,7 +55,8 @@ jobs: - name: Changeset not required if: fromJson(steps.changeset.outputs.CHANGESET).required == false && fromJson(steps.changeset.outputs.CHANGESET).changesetFound == true - uses: marocchino/sticky-pull-request-comment@fcf6fe9e4a0409cd9316a5011435be0f3327f1e1 # ratchet:marocchino/sticky-pull-request-comment@v2.3.1 + # release notes: https://github.com/marocchino/sticky-pull-request-comment/releases/tag/v2.9.0 + uses: marocchino/sticky-pull-request-comment@331f8f5b4215f0445d3c07b4967662a32a2d3e31 # ratchet:marocchino/sticky-pull-request-comment@v2.9.0 with: header: changeset number: ${{ fromJson(steps.changeset.outputs.CHANGESET).pr }} diff --git a/.github/workflows/linkcheck-reporter.yml b/.github/workflows/linkcheck-reporter.yml index 05fe4cbc5f97..856a2e959f9b 100644 --- a/.github/workflows/linkcheck-reporter.yml +++ b/.github/workflows/linkcheck-reporter.yml @@ -26,7 +26,8 @@ jobs: run: echo "pr=$(cat pr)" >> $GITHUB_OUTPUT working-directory: ./results - name: Post report in comment - uses: marocchino/sticky-pull-request-comment@fcf6fe9e4a0409cd9316a5011435be0f3327f1e1 # ratchet:marocchino/sticky-pull-request-comment@v2.3.1 + # release notes: https://github.com/marocchino/sticky-pull-request-comment/releases/tag/v2.9.0 + uses: marocchino/sticky-pull-request-comment@331f8f5b4215f0445d3c07b4967662a32a2d3e31 # ratchet:marocchino/sticky-pull-request-comment@v2.9.0 with: header: linkreport recreate: true diff --git a/.github/workflows/pr-release-branch-warning.yml b/.github/workflows/pr-release-branch-warning.yml index 686d52d96e55..651ef6400702 100644 --- a/.github/workflows/pr-release-branch-warning.yml +++ b/.github/workflows/pr-release-branch-warning.yml @@ -1,6 +1,6 @@ name: Release branch warning on: - pull_request: + pull_request_target: types: - opened @@ -10,16 +10,25 @@ on: branches: - release/client/** - release/server/** + - test/release/** permissions: + # Needed to write the comments to the PRs themselves pull-requests: write jobs: warning: runs-on: ubuntu-latest steps: + - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # ratchet:actions/checkout@v4 + with: + persist-credentials: false + submodules: false + - name: Post warning in comment - uses: marocchino/sticky-pull-request-comment@fcf6fe9e4a0409cd9316a5011435be0f3327f1e1 # ratchet:marocchino/sticky-pull-request-comment@v2.3.1 + # release notes: https://github.com/marocchino/sticky-pull-request-comment/releases/tag/v2.9.0 + uses: marocchino/sticky-pull-request-comment@331f8f5b4215f0445d3c07b4967662a32a2d3e31 # ratchet:marocchino/sticky-pull-request-comment@v2.9.0 with: + header: release-warning path: ${{ github.workspace }}/.github/workflows/data/release-branch-warning.md only_create: true diff --git a/CredScanSuppressions.json b/CredScanSuppressions.json index 133637c79b9e..e32bc2210b2b 100644 --- a/CredScanSuppressions.json +++ b/CredScanSuppressions.json @@ -2,8 +2,16 @@ "tool": "Credential Scanner", "suppressions": [ { - "file": "server/routerlicious/packages/routerlicious/config/telegraf/telegraf.conf", - "_justification": "Sample username password placeholders as part of code comments" + "file": "node_modules/.pnpm/secure-keys@1.0.0/node_modules/secure-keys/test/test.secret.key", + "_justification": "Test file from a dependency" + }, + { + "file": "node_modules/.pnpm/node-rdkafka@3.0.1/node_modules/node-rdkafka/deps/librdkafka/tests/fixtures/ssl/client.keystore.p12", + "_justification": "Test file from a dependency" + }, + { + "file": "node_modules/.pnpm/node-rdkafka@3.0.1/node_modules/node-rdkafka/deps/librdkafka/tests/fixtures/ssl/client2.key", + "_justification": "Test file from a dependency" } ] } diff --git a/PACKAGES.md b/PACKAGES.md index 16080d21c427..aad7d099ab1c 100644 --- a/PACKAGES.md +++ b/PACKAGES.md @@ -189,7 +189,7 @@ The dependencies between layers are enforced by the layer-check command._ | Packages | Layer Dependencies | | --- | --- | -| - [@fluid-example/attributable-map](/examples/apps/attributable-map) (private)
- [@fluid-example/collaborative-textarea](/examples/apps/collaborative-textarea) (private)
- [@fluid-example/contact-collection](/examples/apps/contact-collection) (private)
- [@fluid-example/data-object-grid](/examples/apps/data-object-grid) (private)
- [@fluid-example/presence-tracker](/examples/apps/presence-tracker) (private)
- [@fluid-example/task-selection](/examples/apps/task-selection) (private)
- [@fluid-example/tree-comparison](/examples/apps/tree-comparison) (private)
- [@fluid-example/bubblebench-baseline](/examples/benchmarks/bubblebench/baseline) (private)
- [@fluid-example/bubblebench-common](/examples/benchmarks/bubblebench/common) (private)
- [@fluid-example/bubblebench-experimental-tree](/examples/benchmarks/bubblebench/experimental-tree) (private)
- [@fluid-example/bubblebench-ot](/examples/benchmarks/bubblebench/ot) (private)
- [@fluid-example/bubblebench-shared-tree](/examples/benchmarks/bubblebench/shared-tree) (private)
- [@fluid-example/odspsnapshotfetch-perftestapp](/examples/benchmarks/odspsnapshotfetch-perftestapp) (private)
- [@fluid-internal/tablebench](/examples/benchmarks/tablebench) (private)
- [@fluid-example/app-insights-logger](/examples/client-logger/app-insights-logger) (private)
- [@fluid-example/canvas](/examples/data-objects/canvas) (private)
- [@fluid-example/clicker](/examples/data-objects/clicker) (private)
- [@fluid-example/codemirror](/examples/data-objects/codemirror) (private)
- [@fluid-example/diceroller](/examples/data-objects/diceroller) (private)
- [@fluid-example/inventory-app](/examples/data-objects/inventory-app) (private)
- [@fluid-example/monaco](/examples/data-objects/monaco) (private)
- [@fluid-example/multiview-constellation-model](/examples/data-objects/multiview/constellation-model) (private)
- [@fluid-example/multiview-constellation-view](/examples/data-objects/multiview/constellation-view) (private)
- [@fluid-example/multiview-container](/examples/data-objects/multiview/container) (private)
- [@fluid-example/multiview-coordinate-model](/examples/data-objects/multiview/coordinate-model) (private)
- [@fluid-example/multiview-coordinate-interface](/examples/data-objects/multiview/interface) (private)
- [@fluid-example/multiview-plot-coordinate-view](/examples/data-objects/multiview/plot-coordinate-view) (private)
- [@fluid-example/multiview-slider-coordinate-view](/examples/data-objects/multiview/slider-coordinate-view) (private)
- [@fluid-example/multiview-triangle-view](/examples/data-objects/multiview/triangle-view) (private)
- [@fluid-example/prosemirror](/examples/data-objects/prosemirror) (private)
- [@fluid-example/smde](/examples/data-objects/smde) (private)
- [@fluid-example/table-document](/examples/data-objects/table-document)
- [@fluid-example/todo](/examples/data-objects/todo) (private)
- [@fluid-example/webflow](/examples/data-objects/webflow) (private)
- [@fluid-example/app-integration-external-data](/examples/external-data) (private)
- [@fluid-example/shared-tree-demo](/examples/service-clients/odsp-client/shared-tree-demo) (private)
- [@fluid-example/bundle-size-tests](/examples/utils/bundle-size-tests) (private)
- [@fluid-example/example-utils](/examples/utils/example-utils) (private)
- [@fluid-example/webpack-fluid-loader](/examples/utils/webpack-fluid-loader) (private)
- [@fluid-example/app-integration-live-schema-upgrade](/examples/version-migration/live-schema-upgrade) (private)
- [@fluid-example/version-migration-same-container](/examples/version-migration/same-container) (private)
- [@fluid-example/app-integration-schema-upgrade](/examples/version-migration/schema-upgrade) (private)
- [@fluid-example/tree-shim](/examples/version-migration/tree-shim) (private)
- [@fluid-example/app-integration-container-views](/examples/view-integration/container-views) (private)
- [@fluid-example/app-integration-external-views](/examples/view-integration/external-views) (private)
- [@fluid-example/view-framework-sampler](/examples/view-integration/view-framework-sampler) (private)
- [@fluid-example/property-inspector](/experimental/PropertyDDS/examples/property-inspector) (private)
- [@fluid-example/schemas](/experimental/PropertyDDS/examples/schemas) (private) | - [Core-Interfaces](#Core-Interfaces)
- [Driver-Definitions](#Driver-Definitions)
- [Container-Definitions](#Container-Definitions)
- [Core-Utils](#Core-Utils)
- [Client-Utils](#Client-Utils)
- [Telemetry-Utils](#Telemetry-Utils)
- [Driver-Utils](#Driver-Utils)
- [Other-Utils](#Other-Utils)
- [Tool-Utils](#Tool-Utils)
- [Driver](#Driver)
- [Loader](#Loader)
- [Runtime](#Runtime)
- [Framework](#Framework)
- [UberPackage](#UberPackage)
- [Server-Libs](#Server-Libs)
- [Routerlicious-Driver](#Routerlicious-Driver)
- [Test-Utils](#Test-Utils)
- [ServiceClients](#ServiceClients)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
  | +| - [@fluid-example/attributable-map](/examples/apps/attributable-map) (private)
- [@fluid-example/collaborative-textarea](/examples/apps/collaborative-textarea) (private)
- [@fluid-example/contact-collection](/examples/apps/contact-collection) (private)
- [@fluid-example/data-object-grid](/examples/apps/data-object-grid) (private)
- [@fluid-example/presence-tracker](/examples/apps/presence-tracker) (private)
- [@fluid-example/task-selection](/examples/apps/task-selection) (private)
- [@fluid-example/tree-comparison](/examples/apps/tree-comparison) (private)
- [@fluid-example/bubblebench-baseline](/examples/benchmarks/bubblebench/baseline) (private)
- [@fluid-example/bubblebench-common](/examples/benchmarks/bubblebench/common) (private)
- [@fluid-example/bubblebench-experimental-tree](/examples/benchmarks/bubblebench/experimental-tree) (private)
- [@fluid-example/bubblebench-ot](/examples/benchmarks/bubblebench/ot) (private)
- [@fluid-example/bubblebench-shared-tree](/examples/benchmarks/bubblebench/shared-tree) (private)
- [@fluid-example/odspsnapshotfetch-perftestapp](/examples/benchmarks/odspsnapshotfetch-perftestapp) (private)
- [@fluid-internal/tablebench](/examples/benchmarks/tablebench) (private)
- [@fluid-example/app-insights-logger](/examples/client-logger/app-insights-logger) (private)
- [@fluid-example/canvas](/examples/data-objects/canvas) (private)
- [@fluid-example/clicker](/examples/data-objects/clicker) (private)
- [@fluid-example/codemirror](/examples/data-objects/codemirror) (private)
- [@fluid-example/diceroller](/examples/data-objects/diceroller) (private)
- [@fluid-example/inventory-app](/examples/data-objects/inventory-app) (private)
- [@fluid-example/monaco](/examples/data-objects/monaco) (private)
- [@fluid-example/multiview-constellation-model](/examples/data-objects/multiview/constellation-model) (private)
- [@fluid-example/multiview-constellation-view](/examples/data-objects/multiview/constellation-view) (private)
- [@fluid-example/multiview-container](/examples/data-objects/multiview/container) (private)
- [@fluid-example/multiview-coordinate-model](/examples/data-objects/multiview/coordinate-model) (private)
- [@fluid-example/multiview-coordinate-interface](/examples/data-objects/multiview/interface) (private)
- [@fluid-example/multiview-plot-coordinate-view](/examples/data-objects/multiview/plot-coordinate-view) (private)
- [@fluid-example/multiview-slider-coordinate-view](/examples/data-objects/multiview/slider-coordinate-view) (private)
- [@fluid-example/multiview-triangle-view](/examples/data-objects/multiview/triangle-view) (private)
- [@fluid-example/prosemirror](/examples/data-objects/prosemirror) (private)
- [@fluid-example/smde](/examples/data-objects/smde) (private)
- [@fluid-example/table-document](/examples/data-objects/table-document)
- [@fluid-example/todo](/examples/data-objects/todo) (private)
- [@fluid-example/webflow](/examples/data-objects/webflow) (private)
- [@fluid-example/app-integration-external-data](/examples/external-data) (private)
- [@fluid-example/shared-tree-demo](/examples/service-clients/odsp-client/shared-tree-demo) (private)
- [@fluid-example/bundle-size-tests](/examples/utils/bundle-size-tests) (private)
- [@fluid-example/example-utils](/examples/utils/example-utils) (private)
- [@fluid-example/migration-tools](/examples/utils/migration-tools) (private)
- [@fluid-example/webpack-fluid-loader](/examples/utils/webpack-fluid-loader) (private)
- [@fluid-example/app-integration-live-schema-upgrade](/examples/version-migration/live-schema-upgrade) (private)
- [@fluid-example/version-migration-same-container](/examples/version-migration/same-container) (private)
- [@fluid-example/version-migration-separate-container](/examples/version-migration/separate-container) (private)
- [@fluid-example/tree-shim](/examples/version-migration/tree-shim) (private)
- [@fluid-example/app-integration-container-views](/examples/view-integration/container-views) (private)
- [@fluid-example/app-integration-external-views](/examples/view-integration/external-views) (private)
- [@fluid-example/view-framework-sampler](/examples/view-integration/view-framework-sampler) (private)
- [@fluid-example/property-inspector](/experimental/PropertyDDS/examples/property-inspector) (private)
- [@fluid-example/schemas](/experimental/PropertyDDS/examples/schemas) (private) | - [Core-Interfaces](#Core-Interfaces)
- [Driver-Definitions](#Driver-Definitions)
- [Container-Definitions](#Container-Definitions)
- [Core-Utils](#Core-Utils)
- [Client-Utils](#Client-Utils)
- [Telemetry-Utils](#Telemetry-Utils)
- [Driver-Utils](#Driver-Utils)
- [Other-Utils](#Other-Utils)
- [Tool-Utils](#Tool-Utils)
- [Driver](#Driver)
- [Loader](#Loader)
- [Runtime](#Runtime)
- [Framework](#Framework)
- [UberPackage](#UberPackage)
- [Server-Libs](#Server-Libs)
- [Routerlicious-Driver](#Routerlicious-Driver)
- [Test-Utils](#Test-Utils)
- [ServiceClients](#ServiceClients)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
  | ### Tools diff --git a/RELEASE_NOTES/2.2.0.md b/RELEASE_NOTES/2.2.0.md new file mode 100644 index 000000000000..02b32b0be96c --- /dev/null +++ b/RELEASE_NOTES/2.2.0.md @@ -0,0 +1,516 @@ + + +# Fluid Framework v2.2.0 + +## Contents + +- [✨ New Features](#-new-features) + - [New `isFluidHandle` type guard to check if an object is an `IFluidHandle` (#22029)](#new-isfluidhandle-type-guard-to-check-if-an-object-is-an-ifluidhandle-22029) +- [🌳 SharedTree DDS changes](#-sharedtree-dds-changes) + - [✨ New! When unambiguous, ArrayNodes can now be constructed from Maps and MapNodes from arrays (#22036)](#-new-when-unambiguous-arraynodes-can-now-be-constructed-from-maps-and-mapnodes-from-arrays-22036) + - [✨ New! `Record`-typed objects can now be used to construct MapNodes (#22042)](#-new-record-typed-objects-can-now-be-used-to-construct-mapnodes-22042) + - [Implicit TreeNode construction improvements (#21995)](#implicit-treenode-construction-improvements-21995) + - [Fix document-corrupting bug when rebasing over move compositions (#21993)](#fix-document-corrupting-bug-when-rebasing-over-move-compositions-21993) + - [Enforce use of TreeViewConfiguration's constructor (#22055)](#enforce-use-of-treeviewconfigurations-constructor-22055) + - [New SharedTree configuration option: `ITreeConfigurationOptions.preventAmbiguity` (#22048)](#new-sharedtree-configuration-option-itreeconfigurationoptionspreventambiguity-22048) + - [Add `@alpha` API `FixRecursiveArraySchema` as a workaround around an issue with recursive ArrayNode schema (#22122)](#add-alpha-api-fixrecursivearrayschema-as-a-workaround-around-an-issue-with-recursive-arraynode-schema-22122) + - [Support generation of JSON Schema from Shared Tree view schema (alpha) (#21984)](#support-generation-of-json-schema-from-shared-tree-view-schema-alpha-21984) + - [`Tree.schema` now returns `TreeNodeSchema` (#22185)](#treeschema-now-returns-treenodeschema-22185) + - [Compile-time type narrowing based on a TreeNode's NodeKind (#22222)](#compile-time-type-narrowing-based-on-a-treenodes-nodekind-22222) +- [🐛 Bug Fixes](#-bug-fixes) + - [Recursive SharedTree schemas using MapNodes no longer produce invalid d.ts files (#22106)](#recursive-sharedtree-schemas-using-mapnodes-no-longer-produce-invalid-dts-files-22106) +- [⚠️ Deprecations](#️-deprecations) + - [container-loader: summarizeProtocolTree and its corresponding duplicate ILoaderOptions definition is deprecated (#21999)](#container-loader-summarizeprotocoltree-and-its-corresponding-duplicate-iloaderoptions-definition-is-deprecated-21999) + - [gcThrowOnTombstoneUsage and gcTombstoneEnforcementAllowed are deprecated (#21992)](#gcthrowontombstoneusage-and-gctombstoneenforcementallowed-are-deprecated-21992) + - [InactiveResponseHeaderKey header is deprecated (#22107)](#inactiveresponseheaderkey-header-is-deprecated-22107) + - [The PropertyManager class and related functions and properties are deprecated (#22183)](#the-propertymanager-class-and-related-functions-and-properties-are-deprecated-22183) + - [Deprecate segmentGroups and ack on ISegment (#22183)](#deprecate-segmentgroups-and-ack-on-isegment-22183) +- [Other Changes](#other-changes) + - [Remove PropertyDDS/SharedTree Schema Converter (#22111)](#remove-propertyddssharedtree-schema-converter-22111) + +## ✨ New Features + +### New `isFluidHandle` type guard to check if an object is an `IFluidHandle` ([#22029](https://github.com/microsoft/FluidFramework/issues/22029)) + +The `isFluidHandle` type guard function is now exported and can be used to detect which objects are `IFluidHandle`s. Since `IFluidHandle` often needs special handling (for example when serializing since it's not JSON compatible), having a dedicated detection function for it is useful. Doing this detection was possible previously using the `tree` package's schema system via `Tree.is(value, new SchemaFactory("").handle)`, but can now be done with just `isFluidHandle(value)`. + +#### Change details + +Commit: [`7827d10`](https://github.com/microsoft/FluidFramework/commit/7827d1040a9ebc0bd11388dc31f15370ea9f68d3) + +Affected packages: + +- fluid-framework +- @fluidframework/runtime-utils + +## 🌳 SharedTree DDS changes + +### ✨ New! When unambiguous, ArrayNodes can now be constructed from Maps and MapNodes from arrays ([#22036](https://github.com/microsoft/FluidFramework/issues/22036)) + +Since the types for ArrayNodes and MapNodes indicate they can be constructed from iterables, it should work, even if those iterables are themselves arrays or maps. To avoid this being a breaking change, a priority system was introduced. ArrayNodes will only be implicitly constructable from JavaScript Map objects in contexts where no MapNodes are allowed. Similarly MapNodes will only be implicitly constructable from JavaScript Array objects in contexts where no ArrayNodes are allowed. + +In practice, the main case in which this is likely to matter is when implicitly constructing a map node. If you provide an array of key value pairs, this now works instead of erroring, as long as no ArrayNode is valid at that location in the tree. + +```typescript +class MyMapNode extends schemaFactory.map("x", schemaFactory.number) {} +class Root extends schemaFactory.object("root", { data: MyMapNode }) {} +// This now works (before it compiled, but error at runtime): +const fromArray = new Root({ data: [["x", 5]] }); +``` + +Prior versions used to have to do: + +```typescript +new Root({ data: new MyMapNode([["x", 5]]) }); +``` + +or: + +```typescript +new Root({ data: new Map([["x", 5]]) }); +``` + +Both of these options still work: strictly more cases are allowed with this change. + +#### Change details + +Commit: [`25e74f9`](https://github.com/microsoft/FluidFramework/commit/25e74f9f3bed6e6ff041c088813c4cc1ea276b9c) + +Affected packages: + +- @fluidframework/tree +- fluid-framework + +### ✨ New! `Record`-typed objects can now be used to construct MapNodes ([#22042](https://github.com/microsoft/FluidFramework/issues/22042)) + +You can now construct MapNodes from `Record` typed objects, similar to how maps are expressed in JSON. + +Before this change, an `Iterable` was required, but now an object like `{key1: Child1, key2: Child2}` is allowed. + +Full example using this new API: + +```typescript +class Schema extends schemaFactory.map("ExampleMap", schemaFactory.number) {} +const fromRecord = new Schema({ x: 5 }); +``` + +This new feature makes it possible for schemas to construct a tree entirely from JSON-compatible objects using their constructors, as long as they do not require unhydrated nodes to differentiate ambiguous unions, or IFluidHandles (which themselves are not JSON compatible). + +Due to limitations of TypeScript and recursive types, recursive maps do not advertise support for this feature in their typing, but it works at runtime. + +#### Change details + +Commit: [`25deff3`](https://github.com/microsoft/FluidFramework/commit/25deff344b447380486c1efb64ed69177c32ddc5) + +Affected packages: + +- fluid-framework +- @fluidframework/tree + +### Implicit TreeNode construction improvements ([#21995](https://github.com/microsoft/FluidFramework/issues/21995)) + +ArrayNodes and MapNodes could always be explicitly constructed (using `new`) from iterables. The types also allowed using of iterables to implicitly construct array nodes and map nodes, but this did not work at runtime. This has been fixed for all cases except implicitly constructing an ArrayNode form an `Iterable` that is actually a `Map`, and implicitly constructing a MapNode from an `Iterable` that is actually an `Array`. These cases may be fixed in the future, but require additional work to ensure unions of array nodes and map nodes work correctly. + +Additionally MapNodes can now be constructed from `Iterator` where previously the inner arrays had to be mutable. + +#### Change details + +Commit: [`977f96c`](https://github.com/microsoft/FluidFramework/commit/977f96c1a0dd1d5eb0dbcd087d07cb7510d533ea) + +Affected packages: + +- fluid-framework +- @fluidframework/tree + +### Fix document-corrupting bug when rebasing over move compositions ([#21993](https://github.com/microsoft/FluidFramework/issues/21993)) + +Before this fix, if multiple users concurrently performed moves (possibly by reverting prior moves), there was a chance that the document would become corrupted. + +#### Change details + +Commit: [`f3af9d1`](https://github.com/microsoft/FluidFramework/commit/f3af9d1cd3f7ee1ea3660ae934ddca8473fbdb9b) + +Affected packages: + +- @fluidframework/tree + +### Enforce use of TreeViewConfiguration's constructor ([#22055](https://github.com/microsoft/FluidFramework/issues/22055)) + +`TreeViewConfiguration` is `@sealed`, meaning creating custom implementations of it such as assigning object literals to a `TreeViewConfiguration` or sub-classing it are not supported. This reserved the ability for the Fluid Framework to add members to this class over time, informing users that they must use it in such a way where such changes are non-breaking. However, there was no compiler-based enforcement of this expectation. It was only indicated via documentation and an implicit assumption that when an API takes in a typed defined as a class, that an instance of that class must be used rather than an arbitrary object of a similar shape. + +With this change, the TypeScript compiler will now inform users when they invalidly provide an object literal as a `TreeViewConfiguration`. + +More specifically this causes code like this to produce a compile error: + +```typescript +// Don't do this! +const view = tree.viewWith({ schema: TestNode, enableSchemaValidation: false }); +``` + +The above was never intended to work, and is not a supported use of the `viewWith` since it requires a `TreeViewConfiguration` which is sealed. Any code using the above pattern will break in Fluid Framework 2.2 and above. Such code will need to be updated to the pattern shown below. Any code broken by this change is technically unsupported and only worked due to a gap in the type checking. This is not considered a breaking change. The correct way to get a `TreeViewConfiguration` is by using its constructor: + +```typescript +// This pattern correctly initializes default values and validates input. +const view = tree.viewWith(new TreeViewConfiguration({ schema: TestNode })); +``` + +Skipping the constructor causes the following problems: + +1. `TreeViewConfiguration` does validation in its constructor, so skipping it also skips the validation which leads to much less friendly error messages for invalid schema. +2. Skipping the constructor also discards any default values for options like `enableSchemaValidation`. This means that code written in that style would break if more options were added. Since such changes are planned, it is not practical to support this pattern. + +#### Change details + +Commit: [`e895557`](https://github.com/microsoft/FluidFramework/commit/e8955579f6d52a6c7e300642088c60d6ed12d7db) + +Affected packages: + +- fluid-framework +- @fluidframework/tree + +### New SharedTree configuration option: `ITreeConfigurationOptions.preventAmbiguity` ([#22048](https://github.com/microsoft/FluidFramework/issues/22048)) + +The new `ITreeConfigurationOptions.preventAmbiguity` flag can be set to true to enable checking of some additional rules when constructing the `TreeViewConfiguration`. + +This example shows an ambiguous schema: + +```typescript +const schemaFactory = new SchemaFactory("com.example"); +class Feet extends schemaFactory.object("Feet", { + length: schemaFactory.number, +}) {} +class Meters extends schemaFactory.object("Meters", { + length: schemaFactory.number, +}) {} +const config = new TreeViewConfiguration({ + // This combination of schema can lead to ambiguous cases, and will error since preventAmbiguity is true. + schema: [Feet, Meters], + preventAmbiguity: true, +}); +const view = tree.viewWith(config); +// This is invalid since it is ambiguous which type of node is being constructed. +// The error thrown above when constructing the TreeViewConfiguration is because of this ambiguous case: +view.initialize({ length: 5 }); +``` + +See the documentation on `ITreeConfigurationOptions.preventAmbiguity` for a more complete example and more details. + +#### Change details + +Commit: [`966906a`](https://github.com/microsoft/FluidFramework/commit/966906a03490daa5a914030b37342abb8267c12d) + +Affected packages: + +- fluid-framework +- @fluidframework/tree + +### Add `@alpha` API `FixRecursiveArraySchema` as a workaround around an issue with recursive ArrayNode schema ([#22122](https://github.com/microsoft/FluidFramework/issues/22122)) + +Importing a recursive ArrayNode schema via a d.ts file can produce an error like `error TS2310: Type 'RecursiveArray' recursively references itself as a base type.` if using a tsconfig with `"skipLibCheck": false`. + +This error occurs due to the TypeScript compiler splitting the class definition into two separate declarations in the d.ts file (one for the base, and one for the actual class). For unknown reasons, splitting the class declaration in this way breaks the recursive type handling, leading to the mentioned error. + +Since recursive type handling in TypeScript is order dependent, putting just the right kind of usages of the type before the declarations can cause it to not hit this error. For the case of ArrayNodes, this can be done via usage that looks like this: + +```typescript +/** + * Workaround to avoid + * `error TS2310: Type 'RecursiveArray' recursively references itself as a base type.` in the d.ts file. + */ +export declare const _RecursiveArrayWorkaround: FixRecursiveArraySchema< + typeof RecursiveArray +>; +export class RecursiveArray extends schema.arrayRecursive("RA", [ + () => RecursiveArray, +]) {} +{ + type _check = ValidateRecursiveSchema; +} +``` + +#### Change details + +Commit: [`9ceacf9`](https://github.com/microsoft/FluidFramework/commit/9ceacf9b5468ac8280a1dc48ada9d8b46b499f14) + +Affected packages: + +- @fluidframework/tree + +### Support generation of JSON Schema from Shared Tree view schema (alpha) ([#21984](https://github.com/microsoft/FluidFramework/issues/21984)) + +> WARNING +> +> This API is [alpha quality](https://fluidframework.com/docs/build/releases-and-apitags/#api-support-levels) and may change at any time. + +Adds alpha-quality support for canonical [JSON Schema](https://json-schema.org/docs) representation of Shared Tree schema and adds a `getJsonSchema` function for getting that representation for a given `TreeNodeSchema`. This JSON Schema representation can be used to describe schema requirements to external systems, and can be used with validation tools like [ajv](https://ajv.js.org/) to validate data before inserting it into a `SharedTree`. + +#### Example + +Given a `SharedTree` schema like the following: + +```typescript +class MyObject extends schemaFactory.object("MyObject", { + foo: schemaFactory.number, + bar: schemaFactory.optional(schemaFactory.string), +}); +``` + +JSON Schema like the following would be produced: + +```json +{ + "$defs": { + "com.fluidframework.leaf.string": { + "type": "string" + }, + "com.fluidframework.leaf.number": { + "type": "number" + }, + "com.myapp.MyObject": { + "type": "object", + "properties": { + "foo": { "$ref": "com.fluidframework.leaf.number" }, + "bar": { "$ref": "com.fluidframework.leaf.string" } + }, + "required": ["foo"] + } + }, + "anyOf": [{ "$ref": "#/$defs/com.myapp.MyObject" }] +} +``` + +#### Change details + +Commit: [`9097bf8`](https://github.com/microsoft/FluidFramework/commit/9097bf8a44310d0dcf1a4d2efc3a6f75997c58b3) + +Affected packages: + +- @fluidframework/tree + +### `Tree.schema` now returns `TreeNodeSchema` ([#22185](https://github.com/microsoft/FluidFramework/issues/22185)) + +The typing of `Tree.schema` has changed from: + +```typescript +schema(node: T): TreeNodeSchema; +``` + +to: + +```typescript +schema(node: TreeNode | TreeLeafValue): TreeNodeSchema; +``` + +The runtime behavior is unaffected: any code which worked and still compiles is fine and does not need changes. + +`Tree.schema` was changed to mitigate two different issues: + +1. It tried to give a more specific type based on the type of the passed in value. When the type of the input is not known precisely (for example it is a union of node types like `Foo | Bar`, or `TreeNode` or even `TreeNode | TreeLeafValue`), this was fine since schema are covariant over their node type. However when the input was more specific that the schema type, for example the type is simply `0`, this would result in unsound typing, since the create function could actually return values that did not conform with that schema (for example `schema.create(1)` for the number schema typed with `0` would return `1` with type `0`). +2. The node type was provided to the incorrect type parameter of TreeNodeSchema. The `TNode` parameter is the third one, not the fourth. The fourth is `TBuild` which sets the input accepted to its create function or constructor. Thus this code accidentally left `TNode` unset (which is good due to the above issue), but invalidly set `TBuild`. `TBuild` is contravariant, so it has the opposite issue that setting `TNode` would have: if your input is simply typed as something general like `TreeNode`, then the returned schema would claim to be able to construct an instance given any `TreeNode`. This is incorrect, and this typing has been removed. + +Fortunately it should be rare for code to be impacted by this issue. Any code which manually specified a generic type parameter to `Tree.schema()` will break, as well as code which assigned its result to an overly specifically typed variable. Code which used `typeof` on the returned schema could also break, though there are few use-cases for this so such code is not expected to exist. Currently it's very difficult to invoke the create function or constructor associated with a `TreeNodeSchema` as doing so already requires narrowing to `TreeNodeSchemaClass` or `TreeNodeSchemaNonClass`. It is possible some such code exists which will need to have an explicit cast added because it happened to work with the more specific (but incorrect) constructor input type. + +#### Change details + +Commit: [`bfe8310`](https://github.com/microsoft/FluidFramework/commit/bfe8310a9406a8658c2fac8827c7114844c32234) + +Affected packages: + +- fluid-framework +- @fluidframework/tree + +### Compile-time type narrowing based on a TreeNode's NodeKind ([#22222](https://github.com/microsoft/FluidFramework/issues/22222)) + +`TreeNode`'s schema-aware APIs implement `WithType`, which now has a `NodeKind` parameter that can be used to narrow `TreeNode`s based on `NodeKind`. + +Example: + +```typescript +function getKeys(node: TreeNode & WithType): number[]; +function getKeys( + node: TreeNode & WithType, +): string[]; +function getKeys(node: TreeNode): string[] | number[]; +function getKeys(node: TreeNode): string[] | number[] { + const schema = Tree.schema(node); + switch (schema.kind) { + case NodeKind.Array: { + const arrayNode = node as TreeArrayNode; + const keys: number[] = []; + for (let index = 0; index < arrayNode.length; index++) { + keys.push(index); + } + return keys; + } + case NodeKind.Map: + return [...(node as TreeMapNode).keys()]; + case NodeKind.Object: + return Object.keys(node); + default: + throw new Error("Unsupported Kind"); + } +} +``` + +#### Change details + +Commit: [`4d3bc87`](https://github.com/microsoft/FluidFramework/commit/4d3bc876ae32fa3f2568299e29246f6970e48ee0) + +Affected packages: + +- fluid-framework +- @fluidframework/tree + +## 🐛 Bug Fixes + +### Recursive SharedTree schemas using MapNodes no longer produce invalid d.ts files ([#22106](https://github.com/microsoft/FluidFramework/issues/22106)) + +Consider a recursive SharedTree schema like the following, which follows all our recommended best practices: + +```typescript +export class RecursiveMap extends schema.mapRecursive("RM", [ + () => RecursiveMap, +]) {} +{ + type _check = ValidateRecursiveSchema; +} +``` + +This schema would work when used from within its compilation unit, but would generate d.ts that fails to compile when exporting it: + +```typescript +declare const RecursiveMap_base: import("@fluidframework/tree").TreeNodeSchemaClass< + "com.example.RM", + import("@fluidframework/tree").NodeKind.Map, + import("@fluidframework/tree").TreeMapNodeUnsafe< + readonly [() => typeof RecursiveMap] + > & + import("@fluidframework/tree").WithType<"com.example.RM">, + { + [Symbol.iterator](): Iterator<[string, RecursiveMap], any, undefined>; + }, + false, + readonly [() => typeof RecursiveMap] +>; +export declare class RecursiveMap extends RecursiveMap_base {} +``` + +This results in the compile error in TypeScript 5.4.5: + +> error TS2310: Type 'RecursiveMap' recursively references itself as a base type. + +With this change, that error is fixed by modifying the `TreeMapNodeUnsafe` type it references to inline the definition of `ReadonlyMap` instead of using the one from the TypeScript standard library. + +#### Change details + +Commit: [`554fc5a`](https://github.com/microsoft/FluidFramework/commit/554fc5a94e57e2d109ea9008b7c64517c58a6b73) + +Affected packages: + +- fluid-framework +- @fluidframework/tree + +## ⚠️ Deprecations + +### container-loader: summarizeProtocolTree and its corresponding duplicate ILoaderOptions definition is deprecated ([#21999](https://github.com/microsoft/FluidFramework/issues/21999)) + +The `summarizeProtocolTree` property in ILoaderOptions was added to test single-commit summaries during the initial implementation phase. The flag is no longer required and should no longer be used, and is now marked deprecated. If a driver needs to enable or disable single-commit summaries, it can do so via `IDocumentServicePolicies`. + +#### Change details + +Commit: [`11ccda1`](https://github.com/microsoft/FluidFramework/commit/11ccda15970a10de00facfebfc060bece4a459ba) + +Affected packages: + +- @fluidframework/container-loader + +### gcThrowOnTombstoneUsage and gcTombstoneEnforcementAllowed are deprecated ([#21992](https://github.com/microsoft/FluidFramework/issues/21992)) + +These properties `gcThrowOnTombstoneUsage` and `gcTombstoneEnforcementAllowed` have been deprecated in `IFluidParentContext` and `ContainerRuntime`. These were included in certain garbage collection (GC) telemetry to identify whether the corresponding features have been enabled. These features are now enabled by default and this information is added to the "GarbageCollectorLoaded" telemetry. + +Also, the following Garbage collection runtime options and configs have been removed. They were added during GC feature development to roll out and control functionalities. The corresponding features are on by default and can no longer be disabled or controlled: + +GC runtime options removed: + +- `gcDisableThrowOnTombstoneLoad` +- `disableDataStoreSweep` + +GC configs removed: + +- `"Fluid.GarbageCollection.DisableTombstone"` +- `"Fluid.GarbageCollection.ThrowOnTombstoneUsage"` +- `"Fluid.GarbageCollection.DisableDataStoreSweep"` + +#### Change details + +Commit: [`b2bfed3`](https://github.com/microsoft/FluidFramework/commit/b2bfed3a624d590d776c64a3317c60400b4b3e81) + +Affected packages: + +- @fluidframework/container-runtime +- @fluidframework/runtime-definitions + +### InactiveResponseHeaderKey header is deprecated ([#22107](https://github.com/microsoft/FluidFramework/issues/22107)) + +The header `InactiveResponseHeaderKey` is deprecated and will be removed in the future. It was part of an experimental feature where loading an inactive data store would result in returning a 404 with this header set to true. This feature is no longer supported. + +#### Change details + +Commit: [`2e4e9b2`](https://github.com/microsoft/FluidFramework/commit/2e4e9b2cfcdd7f5d2aa460ab9dedabd6dc2b20ba) + +Affected packages: + +- @fluidframework/container-runtime + +### The PropertyManager class and related functions and properties are deprecated ([#22183](https://github.com/microsoft/FluidFramework/issues/22183)) + +The `PropertyManager` class, along with the `propertyManager` properties and `addProperties` functions on segments and intervals, are not intended for external use. These elements will be removed in a future release for the following reasons: + +- There are no scenarios where they need to be used directly. +- Using them directly will cause eventual consistency problems. +- Upcoming features will require modifications to these mechanisms. + +#### Change details + +Commit: [`cbba695`](https://github.com/microsoft/FluidFramework/commit/cbba69554fc5026f562f44683a902474fabd6e81) + +Affected packages: + +- fluid-framework +- @fluidframework/merge-tree +- @fluidframework/sequence +- @fluid-experimental/sequence-deprecated + +### Deprecate segmentGroups and ack on ISegment ([#22183](https://github.com/microsoft/FluidFramework/issues/22183)) + +The `SegmentGroupCollection` class, along with the `segmentGroups` property and `ack` function on segments, are not intended for external use. These elements will be removed in a future release for the following reasons: + +- There are no scenarios where they need to be used directly. +- Using them directly will cause eventual consistency problems. +- Upcoming features will require modifications to these mechanisms. + +#### Change details + +Commit: [`cbba695`](https://github.com/microsoft/FluidFramework/commit/cbba69554fc5026f562f44683a902474fabd6e81) + +Affected packages: + +- @fluidframework/merge-tree + +## Other Changes + +### Remove PropertyDDS/SharedTree Schema Converter ([#22111](https://github.com/microsoft/FluidFramework/issues/22111)) + +This schema converter had several known issues and has been removed. Read the [schema converter section](https://github.com/microsoft/FluidFramework/blob/main/experimental/PropertyDDS/packages/property-shared-tree-interop/README.md#schema-converter-runtime) of the package readme for more details. + +#### Change details + +Commit: [`54e4b5e`](https://github.com/microsoft/FluidFramework/commit/54e4b5e5ec125b59ebc1c05c93bf55db9cf2921a) + +Affected packages: + +- @fluid-experimental/property-shared-tree-interop + +### 🛠️ Start Building Today! + +Please continue to engage with us on GitHub [Discussion](https://github.com/microsoft/FluidFramework/discussions) and [Issue](https://github.com/microsoft/FluidFramework/issues) pages as you adopt Fluid Framework! diff --git a/azure/packages/azure-local-service/CHANGELOG.md b/azure/packages/azure-local-service/CHANGELOG.md index 4f44dc153075..96ea54b59864 100644 --- a/azure/packages/azure-local-service/CHANGELOG.md +++ b/azure/packages/azure-local-service/CHANGELOG.md @@ -1,5 +1,9 @@ # @fluidframework/azure-local-service +## 2.2.0 + +Dependency updates only. + ## 2.1.0 Dependency updates only. diff --git a/azure/packages/azure-local-service/package.json b/azure/packages/azure-local-service/package.json index 7b1a8f781afb..754a37c274f7 100644 --- a/azure/packages/azure-local-service/package.json +++ b/azure/packages/azure-local-service/package.json @@ -1,6 +1,6 @@ { "name": "@fluidframework/azure-local-service", - "version": "2.2.0", + "version": "2.3.0", "description": "Local implementation of the Azure Fluid Relay service for testing/development use", "homepage": "https://fluidframework.com", "repository": { @@ -38,7 +38,7 @@ "devDependencies": { "@biomejs/biome": "~1.8.3", "@fluidframework/build-common": "^2.0.3", - "@fluidframework/build-tools": "0.43.0-285387", + "@fluidframework/build-tools": "^0.44.0", "@fluidframework/eslint-config-fluid": "^5.3.0", "eslint": "~8.55.0", "eslint-config-prettier": "~9.0.0", diff --git a/azure/packages/azure-service-utils/CHANGELOG.md b/azure/packages/azure-service-utils/CHANGELOG.md index c8afbc86259a..a160431e13df 100644 --- a/azure/packages/azure-service-utils/CHANGELOG.md +++ b/azure/packages/azure-service-utils/CHANGELOG.md @@ -1,5 +1,9 @@ # @fluidframework/azure-service-utils +## 2.2.0 + +Dependency updates only. + ## 2.1.0 Dependency updates only. diff --git a/azure/packages/azure-service-utils/package.json b/azure/packages/azure-service-utils/package.json index 6e61a8afee90..daed02317110 100644 --- a/azure/packages/azure-service-utils/package.json +++ b/azure/packages/azure-service-utils/package.json @@ -1,6 +1,6 @@ { "name": "@fluidframework/azure-service-utils", - "version": "2.2.0", + "version": "2.3.0", "description": "Helper service-side utilities for connecting to Azure Fluid Relay service", "homepage": "https://fluidframework.com", "repository": { @@ -96,10 +96,10 @@ "devDependencies": { "@arethetypeswrong/cli": "^0.15.2", "@biomejs/biome": "~1.8.3", - "@fluid-tools/build-cli": "0.43.0-285387", - "@fluidframework/azure-service-utils-previous": "npm:@fluidframework/azure-service-utils@2.1.0", + "@fluid-tools/build-cli": "^0.44.0", + "@fluidframework/azure-service-utils-previous": "npm:@fluidframework/azure-service-utils@2.2.0", "@fluidframework/build-common": "^2.0.3", - "@fluidframework/build-tools": "0.43.0-285387", + "@fluidframework/build-tools": "^0.44.0", "@fluidframework/eslint-config-fluid": "^5.3.0", "@microsoft/api-extractor": "^7.45.1", "@types/jsrsasign": "^10.5.12", diff --git a/azure/packages/test/scenario-runner/CHANGELOG.md b/azure/packages/test/scenario-runner/CHANGELOG.md index b209d6b4b5d0..f1e5b42dd032 100644 --- a/azure/packages/test/scenario-runner/CHANGELOG.md +++ b/azure/packages/test/scenario-runner/CHANGELOG.md @@ -1,5 +1,9 @@ # @fluid-experimental/azure-scenario-runner +## 2.2.0 + +Dependency updates only. + ## 2.1.0 Dependency updates only. diff --git a/azure/packages/test/scenario-runner/package.json b/azure/packages/test/scenario-runner/package.json index 51715c700a17..357ca315d153 100644 --- a/azure/packages/test/scenario-runner/package.json +++ b/azure/packages/test/scenario-runner/package.json @@ -1,6 +1,6 @@ { "name": "@fluid-experimental/azure-scenario-runner", - "version": "2.2.0", + "version": "2.3.0", "description": "Azure client end to end tests", "homepage": "https://fluidframework.com", "repository": { @@ -93,9 +93,9 @@ }, "devDependencies": { "@biomejs/biome": "~1.8.3", - "@fluid-tools/build-cli": "0.43.0-285387", + "@fluid-tools/build-cli": "^0.44.0", "@fluidframework/build-common": "^2.0.3", - "@fluidframework/build-tools": "0.43.0-285387", + "@fluidframework/build-tools": "^0.44.0", "@fluidframework/eslint-config-fluid": "^5.3.0", "@types/js-yaml": "^4.0.5", "@types/mocha": "^9.1.1", diff --git a/azure/packages/test/scenario-runner/src/packageVersion.ts b/azure/packages/test/scenario-runner/src/packageVersion.ts index af71fae39123..46d164154459 100644 --- a/azure/packages/test/scenario-runner/src/packageVersion.ts +++ b/azure/packages/test/scenario-runner/src/packageVersion.ts @@ -6,4 +6,4 @@ */ export const pkgName = "@fluid-experimental/azure-scenario-runner"; -export const pkgVersion = "2.2.0"; +export const pkgVersion = "2.3.0"; diff --git a/biome.jsonc b/biome.jsonc index 354d91b28eab..6dc671dfdfbd 100644 --- a/biome.jsonc +++ b/biome.jsonc @@ -21,6 +21,7 @@ "**/fluid-runner/src/test/localOdspSnapshots/**", "**/fluid-runner/src/test/telemetryExpectedOutputs/**", "**/snapshots/*.json", + "**/snapshots/content", // Generated files "**/src/**/test/types/*.generated.ts", @@ -39,7 +40,7 @@ "*.hbs", // Test json - "build-tools/packages/build-tools/src/test/data/**", + "build-tools/packages/build-tools/src/test/data/biome/empty.jsonc", "experimental/dds/tree/src/test/documents/**", "packages/dds/map/src/test/mocha/snapshots/**/*.json", "packages/dds/matrix/src/test/results/**/*.json", @@ -94,6 +95,19 @@ // Reports generated by dependency-cruiser "**/.dependency-cruiser-known-violations.json", + // Reports generated by our benchmark tests + ".timeTestsOutput/**", + ".memoryTestsOutput/**", + ".customBenchmarksOutput/**", + + // The paths below are not formatted by Biome. We ignore them explicitly so other tools that read this ignore + // list, like fluid-build, know to ignore these files as well. + "**/*.md", + "**/.gitignore", + "**/.npmignore", + "**/LICENSE", + "**/.changeset/**", + // Paths below are outside the client release group and aren't configured for biome. "common/build/**", "common/lib/**", diff --git a/build-tools/biome.jsonc b/build-tools/biome.jsonc index e0bf13992206..d6a00e2ceb43 100644 --- a/build-tools/biome.jsonc +++ b/build-tools/biome.jsonc @@ -6,5 +6,8 @@ }, "formatter": { "enabled": true + }, + "files": { + "ignore": ["packages/build-tools/src/test/data/biome/empty.jsonc"] } } diff --git a/build-tools/lerna.json b/build-tools/lerna.json index acf30cc14a17..f5ee29356ce5 100644 --- a/build-tools/lerna.json +++ b/build-tools/lerna.json @@ -1 +1 @@ -{ "version": "0.43.0", "npmClient": "pnpm", "useWorkspaces": true } +{ "version": "0.45.0", "npmClient": "pnpm", "useWorkspaces": true } diff --git a/build-tools/package.json b/build-tools/package.json index 4049433f6572..6c846f3c1e6c 100644 --- a/build-tools/package.json +++ b/build-tools/package.json @@ -1,6 +1,6 @@ { "name": "root", - "version": "0.43.0", + "version": "0.45.0", "private": true, "homepage": "https://fluidframework.com", "repository": { diff --git a/build-tools/packages/build-cli/api-report/build-cli.api.md b/build-tools/packages/build-cli/api-report/build-cli.api.md index f55775f5268c..2ea38176d2ce 100644 --- a/build-tools/packages/build-cli/api-report/build-cli.api.md +++ b/build-tools/packages/build-cli/api-report/build-cli.api.md @@ -4,19 +4,119 @@ ```ts +import { InterdependencyRange } from '@fluid-tools/version-tools'; import { run } from '@oclif/core'; +import { VersionBumpType } from '@fluid-tools/version-tools'; -// @internal +// @public (undocumented) +export interface AssertTaggingConfig { + // (undocumented) + assertionFunctions: { + [functionName: string]: number; + }; + enabledPaths?: RegExp[]; +} + +// @public +export interface BumpConfig { + defaultInterdependencyRange?: Record; +} + +// @public +export interface FlubConfig { + assertTagging?: AssertTaggingConfig; + // @deprecated + branchReleaseTypes?: { + [name: string]: VersionBumpType | PreviousVersionStyle; + }; + bump?: BumpConfig; + policy?: PolicyConfig; + releaseNotes?: ReleaseNotesConfig; + version?: 1; +} + +// @public export const knownReleaseGroups: readonly ["build-tools", "client", "server", "gitrest", "historian"]; -// @internal +// @public +export interface PackageNamePolicyConfig { + allowedScopes?: string[]; + mayPublish: { + npm?: string[]; + internalFeed?: string[]; + }; + mustPublish: { + npm?: string[]; + internalFeed?: string[]; + }; + unscopedPackages?: string[]; +} + +// @public +export interface PackageRequirements { + requiredDevDependencies?: string[]; + requiredScripts?: ScriptRequirement[]; +} + +// @public +export interface PolicyConfig { + // (undocumented) + additionalLockfilePaths?: string[]; + // (undocumented) + dependencies?: { + commandPackages: [string, string][]; + }; + exclusions?: string[]; + // (undocumented) + fluidBuildTasks: { + tsc: { + ignoreTasks: string[]; + ignoreDependencies: string[]; + ignoreDevDependencies: string[]; + }; + }; + handlerExclusions?: { + [rule: string]: (string | RegExp)[]; + }; + // (undocumented) + packageNames?: PackageNamePolicyConfig; + // (undocumented) + pnpmSinglePackageWorkspace?: string[]; + publicPackageRequirements?: PackageRequirements; +} + +// @public +export type PreviousVersionStyle = "baseMajor" | "baseMinor" | "previousPatch" | "previousMinor" | "previousMajor" | "~baseMinor" | "^previousMajor" | "^previousMinor" | "~previousMajor" | "~previousMinor"; + +// @public export type ReleaseGroup = (typeof knownReleaseGroups)[number]; +// @public +export interface ReleaseNotesConfig { + // (undocumented) + sections: Record; +} + +// @public +export interface ReleaseNotesSection { + heading: string; +} + +// @public +export type ReleaseNotesSectionName = string; + // @internal export type ReleasePackage = string; export { run } +// @public +export interface ScriptRequirement { + body: string; + bodyMustMatch?: boolean; + name: string; +} + // (No @packageDocumentation comment for this package) ``` diff --git a/build-tools/packages/build-cli/package.json b/build-tools/packages/build-cli/package.json index af290d5030e7..c703af89c965 100644 --- a/build-tools/packages/build-cli/package.json +++ b/build-tools/packages/build-cli/package.json @@ -1,6 +1,6 @@ { "name": "@fluid-tools/build-cli", - "version": "0.43.0", + "version": "0.45.0", "description": "Build tools for the Fluid Framework", "homepage": "https://fluidframework.com", "repository": { @@ -94,6 +94,7 @@ "async": "^3.2.4", "chalk": "^5.3.0", "change-case": "^3.1.0", + "cosmiconfig": "^8.3.6", "danger": "^11.3.0", "date-fns": "^2.30.0", "debug": "^4.3.4", diff --git a/build-tools/packages/build-cli/src/commands/bump.ts b/build-tools/packages/build-cli/src/commands/bump.ts index 19efa09b56f2..968c5b5789ee 100644 --- a/build-tools/packages/build-cli/src/commands/bump.ts +++ b/build-tools/packages/build-cli/src/commands/bump.ts @@ -24,6 +24,7 @@ import { } from "@fluid-tools/version-tools"; import { findPackageOrReleaseGroup, packageOrReleaseGroupArg } from "../args.js"; +import { getDefaultInterdependencyRange } from "../config.js"; import { bumpTypeFlag, checkFlags, skipCheckFlag, versionSchemeFlag } from "../flags.js"; import { BaseCommand, @@ -171,7 +172,8 @@ export default class BumpCommand extends BaseCommand { repoVersion = releaseRepo.version; scheme = flags.scheme ?? detectVersionScheme(repoVersion); // Update the interdependency range to the configured default if the one provided isn't valid - interdependencyRange = interdependencyRange ?? releaseRepo.interdependencyRange; + interdependencyRange = + interdependencyRange ?? getDefaultInterdependencyRange(releaseRepo, context); updatedPackages.push(...releaseRepo.packages); packageOrReleaseGroup = releaseRepo; } else { diff --git a/build-tools/packages/build-cli/src/commands/check/policy.ts b/build-tools/packages/build-cli/src/commands/check/policy.ts index ebac073e7059..48016be75db0 100644 --- a/build-tools/packages/build-cli/src/commands/check/policy.ts +++ b/build-tools/packages/build-cli/src/commands/check/policy.ts @@ -9,8 +9,6 @@ import * as path from "node:path"; import { Flags } from "@oclif/core"; import { readJson } from "fs-extra/esm"; -import { loadFluidBuildConfig } from "@fluidframework/build-tools"; - import { BaseCommand, Context, @@ -166,17 +164,17 @@ export class CheckPolicy extends BaseCommand { this.info("Resolving errors if possible."); } - const manifest = loadFluidBuildConfig(this.flags.root ?? process.cwd()); + const context = await this.getContext(); + const { policy } = context.flubConfig; // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment const rawExclusions: string[] = this.flags.exclusions === undefined - ? manifest.policy?.exclusions + ? policy?.exclusions : await readJson(this.flags.exclusions); const exclusions: RegExp[] = rawExclusions.map((e) => new RegExp(e, "i")); - - const rawHandlerExclusions = manifest?.policy?.handlerExclusions; + const rawHandlerExclusions = policy?.handlerExclusions; const handlerExclusions: HandlerExclusions = {}; if (rawHandlerExclusions) { @@ -186,7 +184,6 @@ export class CheckPolicy extends BaseCommand { } const filePathsToCheck: string[] = []; - const context = await this.getContext(); const gitRoot = context.repo.resolvedRoot; if (this.flags.stdin) { @@ -197,14 +194,8 @@ export class CheckPolicy extends BaseCommand { } } else { const repo = new Repository({ baseDir: gitRoot }); - const gitFiles = await repo.gitClient.raw( - "ls-files", - "-co", - "--exclude-standard", - "--full-name", - ); - - filePathsToCheck.push(...gitFiles.split("\n")); + const gitFiles = await repo.getFiles("."); + filePathsToCheck.push(...gitFiles); } const commandContext: CheckPolicyCommandContext = { diff --git a/build-tools/packages/build-cli/src/commands/generate/assertTags.ts b/build-tools/packages/build-cli/src/commands/generate/assertTags.ts index e9eb8b27c4f3..e47933b7060b 100644 --- a/build-tools/packages/build-cli/src/commands/generate/assertTags.ts +++ b/build-tools/packages/build-cli/src/commands/generate/assertTags.ts @@ -6,7 +6,7 @@ import { strict as assert } from "node:assert"; import fs from "node:fs"; import path from "node:path"; -import { Package, loadFluidBuildConfig } from "@fluidframework/build-tools"; +import { Package } from "@fluidframework/build-tools"; import { PackageCommand } from "../../BasePackageCommand.js"; import { PackageKind } from "../../filter.js"; @@ -54,7 +54,7 @@ export class TagAssertsCommand extends PackageCommand await super.selectAndFilterPackages(); const context = await this.getContext(); - const { assertTagging } = loadFluidBuildConfig(context.gitRepo.resolvedRoot); + const { assertTagging } = context.flubConfig; const assertTaggingEnabledPaths = this.flags.disableConfig ? undefined : assertTagging?.enabledPaths; diff --git a/build-tools/packages/build-cli/src/commands/generate/changeset.ts b/build-tools/packages/build-cli/src/commands/generate/changeset.ts index b0aa05d34163..978d77828b4a 100644 --- a/build-tools/packages/build-cli/src/commands/generate/changeset.ts +++ b/build-tools/packages/build-cli/src/commands/generate/changeset.ts @@ -262,9 +262,9 @@ export default class GenerateChangesetCommand extends BaseCommand< } const sectionChoices: Choice[] = - context.rootFluidBuildConfig.releaseNotes?.sections === undefined + context.flubConfig.releaseNotes?.sections === undefined ? [] - : Object.entries(context.rootFluidBuildConfig.releaseNotes.sections).map( + : Object.entries(context.flubConfig.releaseNotes.sections).map( ([name, { heading }]) => { const choice: Choice = { title: heading, @@ -310,7 +310,7 @@ export default class GenerateChangesetCommand extends BaseCommand< // eslint-disable-next-line @typescript-eslint/no-explicit-any } as any, // This question should only be asked if the releaseNotes config is available - context.rootFluidBuildConfig.releaseNotes === undefined + context.flubConfig.releaseNotes === undefined ? undefined : { name: "section", diff --git a/build-tools/packages/build-cli/src/commands/generate/releaseNotes.ts b/build-tools/packages/build-cli/src/commands/generate/releaseNotes.ts index 13bac29849fb..b637fa979b3e 100644 --- a/build-tools/packages/build-cli/src/commands/generate/releaseNotes.ts +++ b/build-tools/packages/build-cli/src/commands/generate/releaseNotes.ts @@ -5,7 +5,6 @@ import { writeFile } from "node:fs/promises"; import path from "node:path"; -import { type ReleaseNotesSection, loadFluidBuildConfig } from "@fluidframework/build-tools"; import { Flags } from "@oclif/core"; import { StringBuilder } from "@rushstack/node-core-library"; import { format as prettier } from "prettier"; @@ -15,6 +14,7 @@ import remarkGithub, { defaultBuildUrl } from "remark-github"; import admonitions from "remark-github-beta-blockquote-admonitions"; import remarkToc from "remark-toc"; +import { type ReleaseNotesSection } from "../../config.js"; import { releaseGroupFlag } from "../../flags.js"; import { BaseCommand, @@ -88,9 +88,7 @@ export default class GenerateReleaseNotesCommand extends BaseCommand< this.error(`Unknown release group: ${flags.releaseGroup}`, { exit: 2 }); } - const { releaseNotes: releaseNotesConfig } = loadFluidBuildConfig( - context.gitRepo.resolvedRoot, - ); + const { releaseNotes: releaseNotesConfig } = context.flubConfig; if (releaseNotesConfig === undefined) { this.error( `No release notes config found. Make sure the 'releaseNotes' section of the build config exists.`, diff --git a/build-tools/packages/build-cli/src/commands/generate/typetests.ts b/build-tools/packages/build-cli/src/commands/generate/typetests.ts index d8f957ebe1ef..924fdea1f281 100644 --- a/build-tools/packages/build-cli/src/commands/generate/typetests.ts +++ b/build-tools/packages/build-cli/src/commands/generate/typetests.ts @@ -105,37 +105,15 @@ export default class GenerateTypetestsCommand extends PackageCommand< // statements into the type test file, we need to use the previous version name. previousPackageJson.name = previousPackageName; - const { typesPath: currentTypesPathRelative, levelUsed: currentPackageLevel } = - getTypesPathWithFallback(currentPackageJson, level, this.logger, fallbackLevel); - const currentTypesPath = path.resolve(path.join(pkg.directory, currentTypesPathRelative)); - this.verbose( - `Found ${currentPackageLevel} type definitions for ${currentPackageJson.name}: ${currentTypesPath}`, - ); - const { typesPath: previousTypesPathRelative, levelUsed: previousPackageLevel } = getTypesPathWithFallback(previousPackageJson, level, this.logger, fallbackLevel); const previousTypesPath = path.resolve( path.join(previousBasePath, previousTypesPathRelative), ); this.verbose( - `Found ${previousPackageLevel} type definitions for ${previousPackageJson.name}: ${previousTypesPath}`, + `Found ${previousPackageLevel} type definitions for ${currentPackageJson.name}: ${previousTypesPath}`, ); - // For the current version, we load the package-local tsconfig and return index.ts as the source file. This ensures - // we don't need to build before running type test generation. It's tempting to load the .d.ts files and use the - // same code path as is used below for the previous version (loadTypesSourceFile()), but that approach requires that - // the local project be built. - // - // One drawback to this approach is that it will always enumerate the full (internal) API for the current version. - // There's no way to scope it to just alpha, beta, etc. for example. If that capability is eventually needed we can - // revisit this. - const currentFile = new Project({ - skipFileDependencyResolution: true, - tsConfigFilePath: path.join(pkg.directory, "tsconfig.json"), - }).getSourceFileOrThrow("index.ts"); - this.verbose( - `Loaded source file for current version (${pkg.version}): ${currentFile.getFilePath()}`, - ); const previousFile = loadTypesSourceFile(previousTypesPath); this.verbose( `Loaded source file for previous version (${ @@ -143,12 +121,7 @@ export default class GenerateTypetestsCommand extends PackageCommand< }): ${previousFile.getFilePath()}`, ); - const currentTypeMap = typeDataFromFile(currentFile, this.logger); - const previousData = [...typeDataFromFile(previousFile, this.logger).values()]; - - // Sort previous data lexicographically. To use locale-specific sort change the sort function to - // (a, b) => a.name.localeCompare(b.name) - previousData.sort((a, b) => (a.name > b.name ? 1 : a.name < b.name ? -1 : 0)); + const typeMap = typeDataFromFile(previousFile, this.logger); // Sort import statements to respect linting rules. const buildToolsPackageName = "@fluidframework/build-tools"; @@ -180,12 +153,7 @@ declare type MakeUnusedImportErrorsGoAway = TypeOnly | MinimalType | Fu `, ]; - const testCases = generateCompatibilityTestCases( - previousData, - currentTypeMap, - currentPackageJson, - fileHeader, - ); + const testCases = generateCompatibilityTestCases(typeMap, currentPackageJson, fileHeader); mkdirSync(outDir, { recursive: true }); @@ -524,58 +492,52 @@ export function loadTypesSourceFile(typesPath: string): SourceFile { } /** - * Generates compatibility test cases between the previous type definitions and the current type map. - * This function constructs test cases to validate forward and backward compatibility of types. - * @param previousData - array of type data from the previous file - * @param currentTypeMap - map containing current type data + * Generates compatibility test cases using the provided type data to validate forward and backward compatibility of + * types. The type data is assumed to be from an _older_ version of the types. This function will construct test cases + * that import the types from both the old/previous version of a package and the current version and use them in place + * of one another. Failed test cases indicate type incompatibility between versions. + * + * @param typeMap - map containing type data to use to generate type tests * @param packageObject - package.json object containing type validation settings * @param testString - array to store generated test strings * @returns - string array representing generated compatibility test cases */ export function generateCompatibilityTestCases( - previousData: TypeData[], - currentTypeMap: Map, + typeMap: Map, packageObject: PackageJson, testString: string[], ): string[] { const broken: BrokenCompatTypes = packageObject.typeValidation?.broken ?? {}; - for (const oldTypeData of previousData) { - const oldType: TestCaseTypeData = { - prefix: "old", - ...oldTypeData, - removed: false, - }; - const currentTypeData = currentTypeMap.get(oldTypeData.testCaseName); - // if the current package is missing a type, we will use the old type data. - // this can represent a breaking change which can be disable in the package.json. - // this can also happen for type changes, like type to interface, which can remain - // compatible. - const currentType: TestCaseTypeData = - currentTypeData === undefined - ? { - prefix: "current", - ...oldTypeData, - testCaseName: `Removed${oldTypeData.testCaseName}`, - removed: true, - } - : { - prefix: "current", - ...currentTypeData, - removed: false, - }; - - // look for settings not under version, then fall back to version for back compat - const brokenData = broken?.[currentType.testCaseName]; + + // Convert Map entries to an array and sort by key. This is not strictly needed since Maps are iterated in insertion + // order, so the type tests should generate in the same order each time. However, explicitly sorting by the test case + // name is clearer. + const sortedEntries = [...typeMap.entries()].sort((a, b) => a[0].localeCompare(b[0])); + + for (const [testCaseName, typeData] of sortedEntries) { + const [oldType, currentType]: TestCaseTypeData[] = [ + { + prefix: "old", + ...typeData, + removed: false, + }, + { + prefix: "current", + ...typeData, + removed: false, + }, + ]; + const brokenData = broken?.[testCaseName]; const typePreprocessor = selectTypePreprocessor(currentType); if (typePreprocessor !== undefined) { - if (oldTypeData.tags.has("sealed")) { + if (typeData.tags.has("sealed")) { // If the type was `@sealed` then only the code declaring it is allowed to create implementations. // This means that the case of having the new (current) version of the type, // but trying to implement it based on the old version should not occur and is not a supported usage. // This means that adding members to sealed types, as well as making their members have more specific types is allowed as a non-breaking change. // This check implements skipping generation of type tests which would flag such changes to sealed types as errors. - } else if (oldTypeData.useTypeof) { + } else if (typeData.useTypeof) { // If the type was using typeof treat it like `@sealed`. // This assumes adding members to existing variables (and class statics) is non-breaking. // This is true in most cases, though there are some edge cases where this assumption is wrong diff --git a/build-tools/packages/build-cli/src/commands/list.ts b/build-tools/packages/build-cli/src/commands/list.ts index d66700c67b1e..018db0035c3f 100644 --- a/build-tools/packages/build-cli/src/commands/list.ts +++ b/build-tools/packages/build-cli/src/commands/list.ts @@ -107,10 +107,10 @@ export default class ListCommand extends BaseCommand { const filtered = filteredPackages .reverse() .filter((item): item is ListItem => { - const config = context.rootFluidBuildConfig?.policy?.packageNames; + const config = context.flubConfig?.policy?.packageNames; if (config === undefined) { // exits the process - this.error(`No fluid-build package name policy config found.`); + this.error(`No package name policy config found.`); } if (feed === undefined) { diff --git a/build-tools/packages/build-cli/src/config.ts b/build-tools/packages/build-cli/src/config.ts new file mode 100644 index 000000000000..49a8955424cb --- /dev/null +++ b/build-tools/packages/build-cli/src/config.ts @@ -0,0 +1,380 @@ +/*! + * Copyright (c) Microsoft Corporation and contributors. All rights reserved. + * Licensed under the MIT License. + */ + +import { statSync } from "node:fs"; +import { + DEFAULT_INTERDEPENDENCY_RANGE, + InterdependencyRange, + VersionBumpType, +} from "@fluid-tools/version-tools"; +import { MonoRepo } from "@fluidframework/build-tools"; +import { cosmiconfigSync } from "cosmiconfig"; +import { Context } from "./library/index.js"; +import type { ReleaseGroup } from "./releaseGroups.js"; + +/** + * Flub configuration that is expected in the flub config file or package.json. + */ +export interface FlubConfig { + /** + * The version of the config. + * + * IMPORTANT: this will become required in a future release. + * + * @remarks + * + * For backwards-compatibility with the fluidBuild config file - that is, supporting both the flub config and the + * fluidBuild config in the same config file - this value must match the version value of the + * fluidBuildConfig. Once they diverge, the flub config must be separate from the fluidBuild config. + * + * In other words, version 1 is the only version of the configs where they can be stored in the same file. + */ + version?: 1; + + /** + * Ponfiguration for the `check:policy` command. + */ + policy?: PolicyConfig; + + /** + * Configuration for assert tagging. + */ + assertTagging?: AssertTaggingConfig; + + /** + * Configuration for `flub bump`. + */ + bump?: BumpConfig; + + /** + * A mapping of branch names to previous version baseline styles. The type test generator takes this information + * into account when calculating the baseline version to use when it's run on a particular branch. If this is not + * defined for a branch or package, then that package will be skipped during type test generation. + * + * @deprecated This setting is no longer used and will be removed in the future. + */ + branchReleaseTypes?: { + [name: string]: VersionBumpType | PreviousVersionStyle; + }; + + /** + * Configuration for the `generate:releaseNotes` command. + */ + releaseNotes?: ReleaseNotesConfig; +} + +/** + * A type representing the different version constraint styles we use when determining the previous version for type + * test generation. + * + * The "base" versions are calculated by zeroing out all version segments lower than the base. That is, for a version v, + * the baseMajor version is `${v.major}.0.0` and the baseMinor version is `${v.major}.${v.minor}.0`. + * + * The "previous" versions work similarly, but the major/minor/patch segment is reduced by 1. That is, for a version v, + * the previousMajor version is `${min(v.major - 1, 1)}.0.0`, the previousMinor version is + * `${v.major}.${min(v.minor - 1, 0)}.0`, and the previousPatch is `${v.major}.${v.minor}.${min(v.patch - 1, 0)}.0`. + * + * The "previous" versions never roll back below 1 for the major version and 0 for minor and patch. That is, the + * previousMajor, previousMinor, and previousPatch versions for `1.0.0` are all `1.0.0`. + * + * @example + * + * Given the version 2.3.5: + * + * baseMajor: 2.0.0 + * baseMinor: 2.3.0 + * ~baseMinor: ~2.3.0 + * previousPatch: 2.3.4 + * previousMinor: 2.2.0 + * previousMajor: 1.0.0 + * ^previousMajor: ^1.0.0 + * ^previousMinor: ^2.2.0 + * ~previousMajor: ~1.0.0 + * ~previousMinor: ~2.2.0 + * + * @example + * + * Given the version 2.0.0-internal.2.3.5: + * + * baseMajor: 2.0.0-internal.2.0.0 + * baseMinor: 2.0.0-internal.2.3.0 + * ~baseMinor: \>=2.0.0-internal.2.3.0 \<2.0.0-internal.3.0.0 + * previousPatch: 2.0.0-internal.2.3.4 + * previousMinor: 2.0.0-internal.2.2.0 + * previousMajor: 2.0.0-internal.1.0.0 + * ^previousMajor: \>=2.0.0-internal.1.0.0 \<2.0.0-internal.2.0.0 + * ^previousMinor: \>=2.0.0-internal.2.2.0 \<2.0.0-internal.3.0.0 + * ~previousMajor: \>=2.0.0-internal.1.0.0 \<2.0.0-internal.1.1.0 + * ~previousMinor: \>=2.0.0-internal.2.2.0 \<2.0.0-internal.2.2.0 + * + * @example + * + * Given the version 2.0.0-internal.2.0.0: + * + * baseMajor: 2.0.0-internal.2.0.0 + * baseMinor: 2.0.0-internal.2.0.0 + * ~baseMinor: \>=2.0.0-internal.2.0.0 \<2.0.0-internal.2.1.0 + * previousPatch: 2.0.0-internal.2.0.0 + * previousMinor: 2.0.0-internal.2.0.0 + * previousMajor: 2.0.0-internal.1.0.0 + * ^previousMajor: \>=2.0.0-internal.1.0.0 \<2.0.0-internal.2.0.0 + * ^previousMinor: \>=2.0.0-internal.2.0.0 \<2.0.0-internal.3.0.0 + * ~previousMajor: \>=2.0.0-internal.1.0.0 \<2.0.0-internal.1.1.0 + * ~previousMinor: \>=2.0.0-internal.2.0.0 \<2.0.0-internal.2.1.0 + */ +export type PreviousVersionStyle = + | "baseMajor" + | "baseMinor" + | "previousPatch" + | "previousMinor" + | "previousMajor" + | "~baseMinor" + | "^previousMajor" + | "^previousMinor" + | "~previousMajor" + | "~previousMinor"; + +/** + * A short name for the section. Each section in a {@link ReleaseNotesConfig} must have a unique name. + */ +export type ReleaseNotesSectionName = string; + +/** + * Configuration for a release notes section. + */ +export interface ReleaseNotesSection { + /** + * A full string to serve as the heading for the section when displayed in release notes. + */ + heading: string; +} + +/** + * Configuration for the `generate:releaseNotes` command. If this configuration is not present in the config, the + * `generate:releaseNotes` command will report an error. + */ +export interface ReleaseNotesConfig { + sections: Record; +} + +/** + * Policy configuration for the `check:policy` command. + */ +export interface PolicyConfig { + additionalLockfilePaths?: string[]; + pnpmSinglePackageWorkspace?: string[]; + fluidBuildTasks: { + tsc: { + ignoreTasks: string[]; + ignoreDependencies: string[]; + ignoreDevDependencies: string[]; + }; + }; + dependencies?: { + commandPackages: [string, string][]; + }; + /** + * An array of strings/regular expressions. Paths that match any of these expressions will be completely excluded from + * policy-check. + */ + exclusions?: string[]; + + /** + * An object with handler name as keys that maps to an array of strings/regular expressions to + * exclude that rule from being checked. + */ + handlerExclusions?: { [rule: string]: (string | RegExp)[] }; + + packageNames?: PackageNamePolicyConfig; + + /** + * (optional) requirements to enforce against each public package. + */ + publicPackageRequirements?: PackageRequirements; +} + +export interface AssertTaggingConfig { + assertionFunctions: { [functionName: string]: number }; + + /** + * An array of paths under which assert tagging applies to. If this setting is provided, only packages whose paths + * match the regular expressions in this setting will be assert-tagged. + */ + enabledPaths?: RegExp[]; +} + +/** + * Configuration settings that influence `flub bump`. + */ +export interface BumpConfig { + /** + * The interdependencyRange controls the type of semver range to use between packages in the same release group. This + * setting controls the default range that will be used when updating the version of a release group. The default can + * be overridden using the `--interdependencyRange` flag in the `flub bump` command. + */ + defaultInterdependencyRange?: Record; +} + +/** + * Configuration for package naming and publication policies. + */ +export interface PackageNamePolicyConfig { + /** + * A list of package scopes that are permitted in the repo. + */ + allowedScopes?: string[]; + /** + * A list of packages that have no scope. + */ + unscopedPackages?: string[]; + /** + * Packages that must be published. + */ + mustPublish: { + /** + * A list of package names or scopes that must publish to npm, and thus should never be marked private. + */ + npm?: string[]; + + /** + * A list of package names or scopes that must publish to an internal feed, and thus should always be marked + * private. + */ + internalFeed?: string[]; + }; + + /** + * Packages that may or may not be published. + */ + mayPublish: { + /** + * A list of package names or scopes that may publish to npm, and thus might or might not be marked private. + */ + npm?: string[]; + + /** + * A list of package names or scopes that must publish to an internal feed, and thus might or might not be marked + * private. + */ + internalFeed?: string[]; + }; +} + +/** + * Expresses requirements for a given package, applied to its package.json. + */ +export interface PackageRequirements { + /** + * (optional) list of script requirements for the package. + */ + requiredScripts?: ScriptRequirement[]; + + /** + * (optional) list of required dev dependencies for the package. + * @remarks Note: there is no enforcement of version requirements, only that a dependency on the specified name must exist. + */ + requiredDevDependencies?: string[]; +} + +/** + * Requirements for a given script. + */ +export interface ScriptRequirement { + /** + * Name of the script to check. + */ + name: string; + + /** + * Body of the script being checked. + * A contents match will be enforced iff {@link ScriptRequirement.bodyMustMatch}. + * This value will be used as the default contents inserted by the policy resolver (regardless of {@link ScriptRequirement.bodyMustMatch}). + */ + body: string; + + /** + * Whether or not the script body is required to match {@link ScriptRequirement.body} when running the policy checker. + * @defaultValue `false` + */ + bodyMustMatch?: boolean; +} + +const configName = "flub"; + +/** + * A cosmiconfig explorer to find the fluidBuild config. First looks for javascript config files and falls back to the + * fluidBuild property in package.json. We create a single explorer here because cosmiconfig internally caches configs + * for performance. The cache is per-explorer, so re-using the same explorer is a minor perf improvement. + */ +const configExplorer = cosmiconfigSync(configName, { + searchPlaces: [ + // `${configName}.ts`, + `${configName}.config.cjs`, + `${configName}.config.js`, + // Back-compat entries - we'll load settings from the old fluidBuild config files if present. + "fluidBuild.config.cjs", + "fluidBuild.config.js", + "package.json", + ], + packageProp: [ + configName, + // Back-compat entry + "fluidBuild", + ], +}); + +/** + * Get an IFlubConfig from the flub property in a package.json file, or from flub.config.[c]js. + * + * @param configPath - The path to start searching for the config file. If a path to a file is provided, the file will + * be loaded directly. Otherwise it will search upwards looking for config files until it finds one. + * @param noCache - If true, the config cache will be cleared and the config will be reloaded. + * @returns The flub config + */ +export function getFlubConfig(configPath: string, noCache = false): FlubConfig { + if (noCache === true) { + configExplorer.clearCaches(); + } + + // const configResult = configExplorer.search(rootDir); + + const configResult = statSync(configPath).isFile() + ? configExplorer.load(configPath) + : configExplorer.search(configPath); + + const config = configResult?.config as FlubConfig | undefined; + + if (config === undefined) { + throw new Error("No flub configuration found."); + } + + // Only version 1 of the config is supported. If any other value is provided, throw an error. + if ((config.version ?? 1) !== 1) { + throw new Error( + `Configuration version is not supported: ${config?.version}. Config version must be 1.`, + ); + } + + return config; +} + +/** + * Convenience function to extract the default interdependency range for a release group from the flub config. + */ +export function getDefaultInterdependencyRange( + releaseGroup: ReleaseGroup | MonoRepo, + context: Context, +): InterdependencyRange { + const releaseGroupName = releaseGroup instanceof MonoRepo ? releaseGroup.name : releaseGroup; + const interdependencyRangeDefaults = context.flubConfig.bump?.defaultInterdependencyRange; + if (interdependencyRangeDefaults === undefined) { + return DEFAULT_INTERDEPENDENCY_RANGE; + } + + const interdependencyRange = + interdependencyRangeDefaults?.[releaseGroupName as ReleaseGroup]; + + return interdependencyRange ?? DEFAULT_INTERDEPENDENCY_RANGE; +} diff --git a/build-tools/packages/build-cli/src/handlers/doFunctions.ts b/build-tools/packages/build-cli/src/handlers/doFunctions.ts index 988da26810d9..8663a5e7f87a 100644 --- a/build-tools/packages/build-cli/src/handlers/doFunctions.ts +++ b/build-tools/packages/build-cli/src/handlers/doFunctions.ts @@ -11,6 +11,7 @@ import { FluidRepo, MonoRepo } from "@fluidframework/build-tools"; import { bumpVersionScheme, detectVersionScheme } from "@fluid-tools/version-tools"; +import { getDefaultInterdependencyRange } from "../config.js"; import { difference, getPreReleaseDependencies, @@ -177,7 +178,7 @@ export const doReleaseGroupBump: StateHandlerFunction = async ( context, rgRepo, newVersion, - rgRepo instanceof MonoRepo ? rgRepo.interdependencyRange : undefined, + rgRepo instanceof MonoRepo ? getDefaultInterdependencyRange(rgRepo, context) : undefined, log, ); diff --git a/build-tools/packages/build-cli/src/handlers/stateHandlers.ts b/build-tools/packages/build-cli/src/handlers/stateHandlers.ts index 0514baccd358..e00f573700b2 100644 --- a/build-tools/packages/build-cli/src/handlers/stateHandlers.ts +++ b/build-tools/packages/build-cli/src/handlers/stateHandlers.ts @@ -42,7 +42,6 @@ export abstract class BaseStateHandler implements StateHandler { data: unknown, ): Promise; - // eslint-disable-next-line no-useless-constructor public constructor( protected readonly machine: Machine, protected readonly log: CommandLogger, diff --git a/build-tools/packages/build-cli/src/index.ts b/build-tools/packages/build-cli/src/index.ts index 6ccf37ab8082..04bd33ee3c9e 100644 --- a/build-tools/packages/build-cli/src/index.ts +++ b/build-tools/packages/build-cli/src/index.ts @@ -4,4 +4,17 @@ */ export { run } from "@oclif/core"; +export type { + AssertTaggingConfig, + BumpConfig, + FlubConfig, + PackageNamePolicyConfig, + PackageRequirements, + PolicyConfig, + PreviousVersionStyle, + ReleaseNotesConfig, + ReleaseNotesSection, + ReleaseNotesSectionName, + ScriptRequirement, +} from "./config.js"; export type { knownReleaseGroups, ReleaseGroup, ReleasePackage } from "./releaseGroups.js"; diff --git a/build-tools/packages/build-cli/src/library/changesets.ts b/build-tools/packages/build-cli/src/library/changesets.ts index a1d7c06a12e8..fb377a3caf85 100644 --- a/build-tools/packages/build-cli/src/library/changesets.ts +++ b/build-tools/packages/build-cli/src/library/changesets.ts @@ -6,13 +6,14 @@ import { readFile } from "node:fs/promises"; import path from "node:path"; import { VersionBumpType } from "@fluid-tools/version-tools"; -import { Logger, type ReleaseNotesSectionName } from "@fluidframework/build-tools"; +import { Logger } from "@fluidframework/build-tools"; import { compareAsc, formatISO, parseISO } from "date-fns"; import globby from "globby"; import matter from "gray-matter"; import issueParser from "issue-parser"; const { test: hasFrontMatter } = matter; +import type { ReleaseNotesSectionName } from "../config.js"; import { ReleasePackage } from "../releaseGroups.js"; import { Repository } from "./git.js"; diff --git a/build-tools/packages/build-cli/src/library/commands/base.ts b/build-tools/packages/build-cli/src/library/commands/base.ts index 35b2936885e1..eccfa07f1f90 100644 --- a/build-tools/packages/build-cli/src/library/commands/base.ts +++ b/build-tools/packages/build-cli/src/library/commands/base.ts @@ -46,9 +46,6 @@ export abstract class BaseCommand * The flags defined on the base class. */ static readonly baseFlags = { - root: rootPathFlag({ - helpGroup: "GLOBAL", - }), verbose: Flags.boolean({ char: "v", description: "Enable verbose logging.", @@ -64,6 +61,18 @@ export abstract class BaseCommand required: false, default: false, }), + root: Flags.custom({ + description: "Root directory of the Fluid repo (default: env _FLUID_ROOT_).", + env: "_FLUID_ROOT_", + hidden: true, + })(), + flubConfig: Flags.file({ + description: `A path to a flub config file. If this is not provided, it will look up the directory tree to find the closest config file.`, + required: false, + exists: true, + hidden: true, + helpGroup: "GLOBAL", + }), timer: Flags.boolean({ default: false, hidden: true, @@ -137,13 +146,10 @@ export abstract class BaseCommand */ async getContext(): Promise { if (this._context === undefined) { - const resolvedRoot = await (this.flags.root ?? getResolvedFluidRoot()); + const resolvedRoot = await getResolvedFluidRoot(); const gitRepo = new GitRepo(resolvedRoot); const branch = await gitRepo.getCurrentBranchName(); - this.verbose(`Repo: ${resolvedRoot}`); - this.verbose(`Branch: ${branch}`); - this._context = new Context(gitRepo, "microsoft/FluidFramework", branch); } diff --git a/build-tools/packages/build-cli/src/library/context.ts b/build-tools/packages/build-cli/src/library/context.ts index bcf985b746b9..d8f74acbcef9 100644 --- a/build-tools/packages/build-cli/src/library/context.ts +++ b/build-tools/packages/build-cli/src/library/context.ts @@ -5,16 +5,16 @@ import { PackageName } from "@rushstack/node-core-library"; +import { ReleaseVersion } from "@fluid-tools/version-tools"; import { FluidRepo, GitRepo, - IFluidBuildConfig, + type IFluidBuildConfig, Package, - loadFluidBuildConfig, + getFluidBuildConfig, } from "@fluidframework/build-tools"; - -import { ReleaseVersion } from "@fluid-tools/version-tools"; import * as semver from "semver"; +import { type FlubConfig, getFlubConfig } from "../config.js"; /** * Represents a release version and its release date, if applicable. @@ -91,7 +91,8 @@ export function isMonoRepoKind(str: string | undefined): str is MonoRepoKind { export class Context { public readonly repo: FluidRepo; public readonly fullPackageMap: Map; - public readonly rootFluidBuildConfig: IFluidBuildConfig; + public readonly fluidBuildConfig: IFluidBuildConfig; + public readonly flubConfig: FlubConfig; private readonly newBranches: string[] = []; constructor( @@ -99,15 +100,16 @@ export class Context { public readonly originRemotePartialUrl: string, public readonly originalBranchName: string, ) { - // Load the package - this.repo = FluidRepo.create(this.gitRepo.resolvedRoot); + // Load the packages + this.fluidBuildConfig = getFluidBuildConfig(this.gitRepo.resolvedRoot); + this.flubConfig = getFlubConfig(this.gitRepo.resolvedRoot); + this.repo = new FluidRepo(this.gitRepo.resolvedRoot, this.fluidBuildConfig.repoPackages); this.fullPackageMap = this.repo.createPackageMap(); - this.rootFluidBuildConfig = loadFluidBuildConfig(this.repo.resolvedRoot); } /** * Create a branch with name. throw an error if the branch already exist. - * @deprecated ?? + * @deprecated Use GitRepository instead. */ public async createBranch(branchName: string): Promise { // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions diff --git a/build-tools/packages/build-cli/src/library/git.ts b/build-tools/packages/build-cli/src/library/git.ts index bd089b4263a4..ac303eaf98c0 100644 --- a/build-tools/packages/build-cli/src/library/git.ts +++ b/build-tools/packages/build-cli/src/library/git.ts @@ -33,6 +33,7 @@ const defaultGitOptions: Partial = { */ export class Repository { private readonly git: SimpleGit; + private readonly baseDir: string; /** * A git client for the repository that can be used to call git directly. @@ -51,6 +52,7 @@ export class Repository { }; log?.verbose("gitOptions:"); log?.verbose(JSON.stringify(options)); + this.baseDir = options.baseDir; this.git = simpleGit(options); } @@ -207,4 +209,47 @@ export class Repository { return mergeResult.result === "success"; } + + /** + * Returns an array containing repo repo-relative paths to all the files in the provided directory. + * A given path will only be included once in the array; that is, there will be no duplicate paths. + * Note that this function excludes files that are deleted locally whether the deletion is staged or not. + * + * @param directory - A directory to filter the results by. Only files under this directory will be returned. To + * return all files in the repo use the value `"."`. + */ + public async getFiles(directory: string): Promise { + const results = await this.gitClient.raw( + "ls-files", + // Includes cached (staged) files. + "--cached", + // Includes other (untracked) files that are not ignored. + "--others", + // Excludes files that are ignored by standard ignore rules. + "--exclude-standard", + // Removes duplicate entries from the output. + "--deduplicate", + // Shows the full path of the files relative to the repository root. + "--full-name", + "--", + directory, + ); + + // This includes paths to deleted, unstaged files, so we get the list of deleted files from git status and remove + // those from the full list. + const allFiles = new Set( + results + .split("\n") + .map((line) => line.trim()) + // filter out empty lines + .filter((line) => line !== ""), + ); + const status = await this.gitClient.status(); + for (const deletedFile of status.deleted) { + allFiles.delete(deletedFile); + } + + // Files are already repo root-relative + return [...allFiles]; + } } diff --git a/build-tools/packages/build-cli/src/library/layerGraph.ts b/build-tools/packages/build-cli/src/library/layerGraph.ts index 3fb4640626ad..97da5669b151 100644 --- a/build-tools/packages/build-cli/src/library/layerGraph.ts +++ b/build-tools/packages/build-cli/src/library/layerGraph.ts @@ -36,7 +36,6 @@ interface ILayerInfoFile { } class BaseNode { - // eslint-disable-next-line no-useless-constructor constructor(public readonly name: string) {} public get dotName(): string { diff --git a/build-tools/packages/build-cli/src/library/package.ts b/build-tools/packages/build-cli/src/library/package.ts index b92adebf9016..4dcdc3db0df0 100644 --- a/build-tools/packages/build-cli/src/library/package.ts +++ b/build-tools/packages/build-cli/src/library/package.ts @@ -735,16 +735,16 @@ async function findDepUpdates( // Get the new version for each package based on the update type for (const pkgName of dependencies) { let latest: string; - let next: string; + let dev: string; try { // eslint-disable-next-line no-await-in-loop - [latest, next] = await Promise.all([ + [latest, dev] = await Promise.all([ latestVersion(pkgName, { version: "latest", }), latestVersion(pkgName, { - version: "next", + version: "dev", }), ]); } catch (error: unknown) { @@ -752,12 +752,12 @@ async function findDepUpdates( continue; } - // If we're allowing pre-release, use the next tagged version. Warn if it is lower than the latest. + // If we're allowing pre-release, use the version that has the 'dev' dist-tag in npm. Warn if it is lower than the 'latest'. if (prerelease) { - dependencyVersionMap[pkgName] = next; - if (semver.gt(latest, next)) { + dependencyVersionMap[pkgName] = dev; + if (semver.gt(latest, dev)) { log?.warning( - `The latest dist-tag is version ${latest}, which is greater than the next dist-tag version, ${next}. Is this expected?`, + `The 'latest' dist-tag is version ${latest}, which is greater than the 'dev' dist-tag version, ${dev}. Is this expected?`, ); } } else { diff --git a/build-tools/packages/build-cli/src/library/repoPolicyCheck/fluidBuildTasks.ts b/build-tools/packages/build-cli/src/library/repoPolicyCheck/fluidBuildTasks.ts index 6a23316fe95c..15426f6479e8 100644 --- a/build-tools/packages/build-cli/src/library/repoPolicyCheck/fluidBuildTasks.ts +++ b/build-tools/packages/build-cli/src/library/repoPolicyCheck/fluidBuildTasks.ts @@ -12,8 +12,8 @@ import { PackageJson, TscUtils, getEsLintConfigFilePath, + getFluidBuildConfig, getTaskDefinitions, - loadFluidBuildConfig, normalizeGlobalTaskDefinitions, updatePackageJsonFile, updatePackageJsonFileAsync, @@ -21,6 +21,7 @@ import { import JSON5 from "json5"; import * as semver from "semver"; import { TsConfigJson } from "type-fest"; +import { getFlubConfig } from "../../config.js"; import { Handler, readFile } from "./common.js"; import { FluidBuildDatabase } from "./fluidBuildDatabase.js"; @@ -35,8 +36,7 @@ const getFluidBuildTasksTscIgnore = (root: string): Set => { const rootDir = path.resolve(root); let ignore = fluidBuildTasksTscIgnoreTasksCache.get(rootDir); if (ignore === undefined) { - const ignoreArray = - loadFluidBuildConfig(rootDir)?.policy?.fluidBuildTasks?.tsc?.ignoreTasks; + const ignoreArray = getFlubConfig(rootDir)?.policy?.fluidBuildTasks?.tsc?.ignoreTasks; ignore = ignoreArray ? new Set(ignoreArray) : new Set(); fluidBuildTasksTscIgnoreTasksCache.set(rootDir, ignore); } @@ -51,7 +51,8 @@ function getFluidPackageMap(root: string): Map { const rootDir = path.resolve(root); let record = repoCache.get(rootDir); if (record === undefined) { - const repo = FluidRepo.create(rootDir); + const fluidBuildConfig = getFluidBuildConfig(rootDir); + const repo = new FluidRepo(rootDir, fluidBuildConfig.repoPackages); const packageMap = repo.createPackageMap(); record = { repo, packageMap }; repoCache.set(rootDir, record); @@ -335,7 +336,7 @@ function hasTaskDependency( taskName: string, searchDeps: readonly string[], ): boolean { - const rootConfig = loadFluidBuildConfig(root); + const rootConfig = getFluidBuildConfig(root); const globalTaskDefinitions = normalizeGlobalTaskDefinitions(rootConfig?.tasks); const taskDefinitions = getTaskDefinitions(json, globalTaskDefinitions, false); // Searched deps that are package specific (e.g. #) @@ -454,7 +455,7 @@ function patchTaskDeps( let tasks: Exclude["tasks"], undefined>; if (json.fluidBuild === undefined) { tasks = {}; - json.fluidBuild = { tasks }; + json.fluidBuild = { tasks, version: 1 }; } else if (json.fluidBuild.tasks === undefined) { tasks = {}; json.fluidBuild.tasks = tasks; diff --git a/build-tools/packages/build-cli/src/library/repoPolicyCheck/lockfiles.ts b/build-tools/packages/build-cli/src/library/repoPolicyCheck/lockfiles.ts index 47c96156a9a2..f0403459d6a1 100644 --- a/build-tools/packages/build-cli/src/library/repoPolicyCheck/lockfiles.ts +++ b/build-tools/packages/build-cli/src/library/repoPolicyCheck/lockfiles.ts @@ -5,29 +5,31 @@ import { unlinkSync } from "node:fs"; import path from "node:path"; -import { IFluidBuildConfig, loadFluidBuildConfig } from "@fluidframework/build-tools"; +import type { IFluidBuildConfig } from "@fluidframework/build-tools"; +import { getFluidBuildConfig } from "@fluidframework/build-tools"; +import { FlubConfig, getFlubConfig } from "../../config.js"; import { Handler } from "./common.js"; const lockFilePattern = /.*?package-lock\.json$/i; let _knownPaths: string[] | undefined; -const getKnownPaths = (manifest: IFluidBuildConfig): string[] => { +const getKnownPaths = (config: FlubConfig, repoConfig: IFluidBuildConfig): string[] => { if (_knownPaths === undefined) { // Add the root path (.) because a lockfile is expected there _knownPaths = ["."]; // Add additional paths from the manifest - _knownPaths.push(...(manifest.policy?.additionalLockfilePaths ?? [])); + _knownPaths.push(...(config.policy?.additionalLockfilePaths ?? [])); - if (manifest.repoPackages) { + if (repoConfig.repoPackages) { // Add paths to known monorepos and packages - const vals = Object.values(manifest.repoPackages).filter( + const vals = Object.values(repoConfig.repoPackages).filter( (p) => typeof p === "string", ) as string[]; _knownPaths.push(...vals); // Add paths from entries that are arrays - const arrayVals = Object.values(manifest.repoPackages).filter( + const arrayVals = Object.values(repoConfig.repoPackages).filter( (p) => typeof p !== "string", ); for (const arr of arrayVals) { @@ -46,8 +48,9 @@ export const handlers: Handler[] = [ name: "extraneous-lockfiles", match: lockFilePattern, handler: async (file: string, root: string): Promise => { - const manifest = loadFluidBuildConfig(root); - const knownPaths: string[] = getKnownPaths(manifest); + const flubConfig = getFlubConfig(root); + const repoConfig = getFluidBuildConfig(root); + const knownPaths: string[] = getKnownPaths(flubConfig, repoConfig); if ( path.basename(file) === "package-lock.json" && @@ -59,8 +62,9 @@ export const handlers: Handler[] = [ return undefined; }, resolver: (file: string, root: string): { resolved: boolean; message?: string } => { - const manifest = loadFluidBuildConfig(root); - const knownPaths: string[] = getKnownPaths(manifest); + const flubConfig = getFlubConfig(root); + const repoConfig = getFluidBuildConfig(root); + const knownPaths: string[] = getKnownPaths(flubConfig, repoConfig); if ( path.basename(file) === "package-lock.json" && diff --git a/build-tools/packages/build-cli/src/library/repoPolicyCheck/npmPackages.ts b/build-tools/packages/build-cli/src/library/repoPolicyCheck/npmPackages.ts index 3c2c62149b0e..bd6cf9325d7f 100644 --- a/build-tools/packages/build-cli/src/library/repoPolicyCheck/npmPackages.ts +++ b/build-tools/packages/build-cli/src/library/repoPolicyCheck/npmPackages.ts @@ -10,23 +10,22 @@ import fs from "node:fs"; import { createRequire } from "node:module"; import { EOL as newline } from "node:os"; import path from "node:path"; -import * as readline from "node:readline"; import { writeJson } from "fs-extra/esm"; import replace from "replace-in-file"; import sortPackageJson from "sort-package-json"; import { PackageJson, - PackageNamePolicyConfig, - ScriptRequirement, getApiExtractorConfigFilePath, - loadFluidBuildConfig, updatePackageJsonFile, updatePackageJsonFileAsync, } from "@fluidframework/build-tools"; +import { Repository } from "../git.js"; import { queryTypesResolutionPathsFromPackageExports } from "../packageExports.js"; import { Handler, readFile, writeFile } from "./common.js"; +import { PackageNamePolicyConfig, ScriptRequirement, getFlubConfig } from "../../config.js"; + const require = createRequire(import.meta.url); const licenseId = "MIT"; @@ -155,7 +154,7 @@ export function packageMayChooseToPublishToInternalFeedOnly( * private to prevent publishing. */ export function packageMustBePrivate(name: string, root: string): boolean { - const config = loadFluidBuildConfig(root).policy?.packageNames; + const config = getFlubConfig(root).policy?.packageNames; if (config === undefined) { // Unless configured, all packages must be private @@ -174,7 +173,7 @@ export function packageMustBePrivate(name: string, root: string): boolean { * If we know a package needs to publish somewhere, then it must not be marked private to allow publishing. */ export function packageMustNotBePrivate(name: string, root: string): boolean { - const config = loadFluidBuildConfig(root).policy?.packageNames; + const config = getFlubConfig(root).policy?.packageNames; if (config === undefined) { // Unless configured, all packages must be private @@ -190,7 +189,7 @@ export function packageMustNotBePrivate(name: string, root: string): boolean { * Whether the package either belongs to a known Fluid package scope or is a known unscoped package. */ function packageIsFluidPackage(name: string, root: string): boolean { - const config = loadFluidBuildConfig(root).policy?.packageNames; + const config = getFlubConfig(root).policy?.packageNames; if (config === undefined) { // Unless configured, all packages are considered Fluid packages @@ -354,39 +353,25 @@ function getReadmeInfo(dir: string): IReadmeInfo { } let computedPrivatePackages: Set | undefined; -function ensurePrivatePackagesComputed(): Set { - if (computedPrivatePackages) { +async function ensurePrivatePackagesComputed(): Promise> { + if (computedPrivatePackages !== undefined) { return computedPrivatePackages; } - const newPrivatePackages = new Set(); + computedPrivatePackages = new Set(); const pathToGitRoot = child_process .execSync("git rev-parse --show-cdup", { encoding: "utf8" }) .trim(); - const p = child_process.spawn("git", [ - "ls-files", - "-co", - "--exclude-standard", - "--full-name", - "**/package.json", - ]); - const lineReader = readline.createInterface({ - input: p.stdout, - terminal: false, - }); - - lineReader.on("line", (line) => { - const filePath = path.join(pathToGitRoot, line).trim().replace(/\\/g, "/"); - if (fs.existsSync(filePath)) { - const packageJson = JSON.parse(readFile(filePath)) as PackageJson; - // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions - if (packageJson.private) { - newPrivatePackages.add(packageJson.name); - } + const repo = new Repository({ baseDir: pathToGitRoot }); + const packageJsons = await repo.getFiles("**/package.json"); + + for (const filePath of packageJsons) { + const packageJson = JSON.parse(readFile(filePath)) as PackageJson; + if (packageJson.private ?? false) { + computedPrivatePackages.add(packageJson.name); } - }); + } - computedPrivatePackages = newPrivatePackages; return computedPrivatePackages; } @@ -914,7 +899,7 @@ export const handlers: Handler[] = [ return `Error parsing JSON file: ${file}`; } - const privatePackages = ensurePrivatePackagesComputed(); + const privatePackages = await ensurePrivatePackagesComputed(); const errors: string[] = []; // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions @@ -1215,7 +1200,7 @@ export const handlers: Handler[] = [ name: "npm-package-json-script-dep", match, handler: async (file: string, root: string): Promise => { - const manifest = loadFluidBuildConfig(root); + const manifest = getFlubConfig(root); const commandPackages = manifest.policy?.dependencies?.commandPackages; if (commandPackages === undefined) { return; @@ -1881,8 +1866,7 @@ export const handlers: Handler[] = [ return; } - const requirements = - loadFluidBuildConfig(rootDirectoryPath).policy?.publicPackageRequirements; + const requirements = getFlubConfig(rootDirectoryPath).policy?.publicPackageRequirements; if (requirements === undefined) { // If no requirements have been specified, we have nothing to validate. return; @@ -1939,7 +1923,7 @@ export const handlers: Handler[] = [ } const requirements = - loadFluidBuildConfig(rootDirectoryPath).policy?.publicPackageRequirements; + getFlubConfig(rootDirectoryPath).policy?.publicPackageRequirements; if (requirements === undefined) { // If no requirements have been specified, we have nothing to validate. return; diff --git a/build-tools/packages/build-cli/src/library/repoPolicyCheck/pnpm.ts b/build-tools/packages/build-cli/src/library/repoPolicyCheck/pnpm.ts index bc74ebe01857..9ce91d98b44f 100644 --- a/build-tools/packages/build-cli/src/library/repoPolicyCheck/pnpm.ts +++ b/build-tools/packages/build-cli/src/library/repoPolicyCheck/pnpm.ts @@ -5,7 +5,8 @@ import fs from "node:fs"; import path from "node:path"; -import { PackageJson, loadFluidBuildConfig } from "@fluidframework/build-tools"; +import { PackageJson } from "@fluidframework/build-tools"; +import { getFlubConfig } from "../../config.js"; import { Handler, readFile } from "./common.js"; const match = /(?:^|\/)pnpm-lock\.yaml$/i; @@ -17,7 +18,7 @@ export const handlers: Handler[] = [ handler: async (file: string, root: string): Promise => { const dirname = path.dirname(file); const packageJsonFile = path.join(dirname, "package.json"); - const manifest = loadFluidBuildConfig(root); + const manifest = getFlubConfig(root); let json: PackageJson; try { diff --git a/build-tools/packages/build-cli/src/releaseGroups.ts b/build-tools/packages/build-cli/src/releaseGroups.ts index bcb33cbcad31..d1e7690deae7 100644 --- a/build-tools/packages/build-cli/src/releaseGroups.ts +++ b/build-tools/packages/build-cli/src/releaseGroups.ts @@ -17,8 +17,6 @@ export type ReleasePackage = string; /** * An array of known release groups. - * - * @internal */ export const knownReleaseGroups = [ "build-tools", @@ -30,15 +28,11 @@ export const knownReleaseGroups = [ /** * A type that represents release groups. - * - * @internal */ export type ReleaseGroup = (typeof knownReleaseGroups)[number]; /** * A type guard used to determine if a string is a ReleaseGroup. - * - * @internal */ export function isReleaseGroup(str: string | undefined): str is ReleaseGroup { // eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-explicit-any diff --git a/build-tools/packages/build-cli/test/commands/list.test.ts b/build-tools/packages/build-cli/test/commands/list.test.ts index 4dc31318ed8a..fd803d7cba66 100644 --- a/build-tools/packages/build-cli/test/commands/list.test.ts +++ b/build-tools/packages/build-cli/test/commands/list.test.ts @@ -3,14 +3,10 @@ * Licensed under the MIT License. */ -import { - GitRepo, - type Package, - type PackageNamePolicyConfig, - getResolvedFluidRoot, -} from "@fluidframework/build-tools"; +import { GitRepo, type Package, getResolvedFluidRoot } from "@fluidframework/build-tools"; import { expect } from "chai"; +import { type PackageNamePolicyConfig } from "../../src/config.js"; import { Context } from "../../src/library/index.js"; import { type Feed, @@ -52,7 +48,7 @@ describe("feeds", async () => { const branch = await gitRepo.getCurrentBranchName(); const context = new Context(gitRepo, "microsoft/FluidFramework", branch); - const config = context.rootFluidBuildConfig?.policy?.packageNames!; + const config = context.flubConfig.policy?.packageNames!; const packages = FeedsForPackages(context.packages, config); it("dev and build feed are mutually exclusive", () => { diff --git a/build-tools/packages/build-tools/biome.jsonc b/build-tools/packages/build-tools/biome.jsonc new file mode 100644 index 000000000000..504f9419b45f --- /dev/null +++ b/build-tools/packages/build-tools/biome.jsonc @@ -0,0 +1,7 @@ +{ + "$schema": "./node_modules/@biomejs/biome/configuration_schema.json", + "extends": ["../../../biome.jsonc"], + "files": { + "ignore": ["src/test/data/biome/empty.jsonc"] + } +} diff --git a/build-tools/packages/build-tools/package.json b/build-tools/packages/build-tools/package.json index 84dfdf5d2aef..a8ec207cb8b2 100644 --- a/build-tools/packages/build-tools/package.json +++ b/build-tools/packages/build-tools/package.json @@ -1,6 +1,6 @@ { "name": "@fluidframework/build-tools", - "version": "0.43.0", + "version": "0.45.0", "description": "Fluid Build tools", "homepage": "https://fluidframework.com", "repository": { @@ -30,6 +30,7 @@ "eslint:fix": "eslint --format stylish src --fix --fix-type problem,suggestion,layout", "format": "npm run format:biome", "format:biome": "biome check --write .", + "json-schema-to-typescript:biomeConfigTypes": "json2ts --input node_modules/@biomejs/biome/configuration_schema.json --output src/common/biomeConfigTypes.d.ts", "lint": "npm run eslint", "lint:fix": "npm run eslint:fix", "list-repo-files": "cd ../../.. && git ls-files -co --exclude-standard", @@ -38,11 +39,10 @@ "tsc": "tsc" }, "dependencies": { - "@fluid-tools/version-tools": "workspace:~", "@manypkg/get-packages": "^2.2.0", "async": "^3.2.4", "chalk": "^2.4.2", - "cosmiconfig": "^8.2.0", + "cosmiconfig": "^8.3.6", "date-fns": "^2.30.0", "debug": "^4.3.4", "detect-indent": "^6.1.0", @@ -53,10 +53,12 @@ "json5": "^2.2.3", "lodash": "^4.17.21", "lodash.isequal": "^4.5.0", + "multimatch": "^5.0.0", "picomatch": "^2.3.1", "rimraf": "^4.4.1", "semver": "^7.5.4", "sort-package-json": "1.57.0", + "ts-deepmerge": "^7.0.0", "ts-morph": "^22.0.0", "type-fest": "^2.19.0", "typescript": "~5.4.5", @@ -79,6 +81,7 @@ "@types/shelljs": "^0.8.12", "concurrently": "^8.2.1", "eslint": "~8.57.0", + "json-schema-to-typescript": "^15.0.0", "mocha": "^10.2.0" }, "engines": { diff --git a/build-tools/packages/build-tools/src/common/biomeConfig.ts b/build-tools/packages/build-tools/src/common/biomeConfig.ts new file mode 100644 index 000000000000..d7832814aa62 --- /dev/null +++ b/build-tools/packages/build-tools/src/common/biomeConfig.ts @@ -0,0 +1,269 @@ +/*! + * Copyright (c) Microsoft Corporation and contributors. All rights reserved. + * Licensed under the MIT License. + */ + +import assert from "node:assert/strict"; +import { readFile, stat } from "node:fs/promises"; +import path from "node:path"; +import ignore from "ignore"; +import * as JSON5 from "json5"; +import multimatch from "multimatch"; +import { merge } from "ts-deepmerge"; +// Note: in more recent versions of type-fest, this type has been replaced with "Tagged" +// We are using version 2.x because of this issue: https://github.com/sindresorhus/type-fest/issues/547 +import type { Opaque } from "type-fest"; +import type { Configuration as BiomeConfigRaw } from "./biomeConfigTypes"; +import type { GitRepo } from "./gitRepo"; + +// switch to regular import once building ESM +const findUp = import("find-up"); + +/** + * Convenience type to represent a Biome config that has been loaded while following and merging the + * "extends" values. This helps differentiate between the single loaded configs and the fully resolved config. + */ +export type BiomeConfigResolved = Opaque; + +/** + * Loads a Biome configuration file _without_ following any 'extends' values. You probably want to use + * {@link loadBiomeConfigs} instead of this function. + */ +async function loadRawBiomeConfig(configPath: string): Promise { + const contents = await readFile(configPath, "utf8"); + const config: BiomeConfigRaw = JSON5.parse(contents); + return config; +} + +/** + * Returns an array of absolute paths to Biome config files. The paths are in the order in which they are merged by + * Biome. That is, the last item in the array will be the absolute path to `configPath`. + */ +export async function getAllBiomeConfigPaths(configPath: string): Promise { + const config = await loadRawBiomeConfig(configPath); + let extendedConfigPaths: string[] = []; + + if (config.extends) { + const pathsNested = await Promise.all( + config.extends.map((configToExtend) => + getAllBiomeConfigPaths(path.join(path.dirname(configPath), configToExtend)), + ), + ); + extendedConfigPaths = pathsNested.flat(); + } + + // Add the current config as the last one to be applied when they're merged + extendedConfigPaths.push(configPath); + return extendedConfigPaths; +} + +/** + * Loads a Biome configuration file. If the config extends others, then those are loaded recursively and the results are + * merged. Array-type values are not merged, in accordance with how Biome applies configs. + * + * @remarks + * + * The intent is to merge the configs in the same way that Biome itself does, but the implementation is based on the + * Biome documentation, so there may be subtle differences unaccounted for. Where this implementation diverges from + * Biome's behavior, this function should be considered incorrect. + * + * Relevant Biome documentation: {@link https://biomejs.dev/guides/configure-biome/#share-a-configuration-file} + */ +export async function loadBiomeConfig(configPath: string): Promise { + const allConfigPaths = await getAllBiomeConfigPaths(configPath); + return loadBiomeConfigs(allConfigPaths); +} + +/** + * Loads a set of Biome configs, such as that returned by {@link getAllBiomeConfigPaths}. The configs are loaded + * recursively and the results are merged. Array-type values are not merged, in accordance with how Biome applies + * configs. + */ +async function loadBiomeConfigs(allConfigPaths: string[]): Promise { + const allConfigs = await Promise.all( + allConfigPaths.map((pathToConfig) => loadRawBiomeConfig(pathToConfig)), + ); + + const mergedConfig = merge.withOptions( + { + // Biome does not merge arrays + mergeArrays: false, + }, + ...allConfigs, + ); + + return mergedConfig as BiomeConfigResolved; +} + +export type BiomeIncludeIgnore = "include" | "ignore"; +export type BiomeConfigSection = "formatter" | "linter"; + +/** + * Given a Biome config object, returns the combined settings for 'ignore' and 'include' across the 'files', 'formatter' + * and 'linter' sections in the config. + */ +export function getSettingValuesFromBiomeConfig( + config: BiomeConfigResolved, + section: BiomeConfigSection, + kind: BiomeIncludeIgnore, +): Set { + const generalFiles = config.files?.[kind] ?? []; + const sectionFiles = config?.[section]?.[kind] ?? []; + return new Set([...generalFiles, ...sectionFiles]); +} + +/** + * Returns the absolute path to the closest Biome config file found from the current working directory up to the root + * of the repo. + * + * @throws If a Biome config file cannot be found. + */ +export async function getClosestBiomeConfigPath( + cwd: string, + stopAt?: string, +): Promise { + return (await findUp) + .findUp(["biome.json", "biome.jsonc"], { cwd, stopAt }) + .then((config) => { + if (config === undefined) { + throw new Error(`Can't find biome config file`); + } + return config; + }); +} + +/** + * Return an array of absolute paths to files that Biome would format under the provided path. Note that .gitignored + * paths are always excluded, regardless of the "vcs" setting in the Biome configuration. + * + * @param directoryOrConfigFile - A path to a directory or a Biome config file. If a directory is provided, then the + * closest Biome configuration will be loaded and used. If a path to a file is provided, it is assumed to be a Biome + * config file and will be loaded as such. The directory containing the config file will be used as the working + * directory when applying the Biome include/ignore settings. + * @param gitRepo - A GitRepo instance that is used to enumerate files. + */ +export async function getBiomeFormattedFilesFromDirectory( + directoryOrConfigFile: string, + gitRepo: GitRepo, +): Promise { + /** + * The repo root-relative path to the directory being used as the Biome working directory. + */ + let directory: string; + let configFile: string; + if ((await stat(directoryOrConfigFile)).isFile()) { + configFile = directoryOrConfigFile; + directory = path.relative(gitRepo.resolvedRoot, path.dirname(directoryOrConfigFile)); + } else { + configFile = await getClosestBiomeConfigPath(directoryOrConfigFile); + directory = path.relative(gitRepo.resolvedRoot, directoryOrConfigFile); + } + const config = await loadBiomeConfig(configFile); + return getBiomeFormattedFiles(config, directory, gitRepo); +} + +/** + * Return an array of absolute paths to files that Biome would format under the provided path. Note that .gitignored + * paths are always excluded, regardless of the "vcs" setting in the Biome configuration. + * + * @param config - A resolved/merged Biome config. + * @param directory - The directory containing files to be formatted. + * @param gitRepo - A GitRepo instance that is used to enumerate files. + */ +export async function getBiomeFormattedFiles( + config: BiomeConfigResolved, + directory: string, + gitRepo: GitRepo, +): Promise { + const [includeEntries, ignoreEntries] = await Promise.all([ + getSettingValuesFromBiomeConfig(config, "formatter", "include"), + getSettingValuesFromBiomeConfig(config, "formatter", "ignore"), + ]); + + // From the Biome docs (https://biomejs.dev/guides/how-biome-works/#include-and-ignore-explained): + // + // "At the moment, Biome uses a glob library that treats all globs as having a **/ prefix. + // This means that src/**/*.js and **/src/**/*.js are treated as identical. They match both src/file.js and + // test/src/file.js. This is something we plan to fix in Biome v2.0.0." + const prefixedIncludes = [...includeEntries].map((glob) => `**/${glob}`); + const prefixedIgnores = [...ignoreEntries].map((glob) => `**/${glob}`); + + /** + * All files that could possibly be formatted before Biome include and ignore entries are applied. Paths are relative + * to the root of the repo. + */ + const gitLsFiles = new Set(await gitRepo.getFiles(directory)); + + /** + * An array of repo-relative paths to files included via the 'include' settings in the Biome config. + */ + const includedPaths = + prefixedIncludes.length > 0 + ? // If there are includes, then we filter the possible files using the include globs + multimatch([...gitLsFiles], prefixedIncludes) + : // No Biome includes were provided, so we include everything git enumerated + [...gitLsFiles]; + + const ignoreObject = ignore().add(prefixedIgnores); + // Note that ignoreObject.filter expects the paths to be relative to the repo root. + const filtered = ignoreObject.filter(includedPaths); + + // Convert repo root-relative paths to absolute paths + const repoRoot = gitRepo.resolvedRoot; + return filtered.map((filePath) => path.resolve(repoRoot, filePath)); +} + +/** + * A class used to simplify access to a Biome config when you want to just load a config and get the file list and + * config details. Given a directory and a GitRepo instance, the class calculates and caches the configs and formatted + * files. Using this class can be more convenient than using the free functions, especially when you need access to all + * the configs and formatted files. + */ +export class BiomeConfigReader { + public get closestConfig(): string { + assert( + this.allConfigs.length > 0, + "BiomeConfigLoader.allConfigs must be initialized before getting the closestConfig.", + ); + // The closest config is the last one in the list of configs, because they're sorted in the order they're applied. + // We previously asserted that there is at least one element in the array + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + return this.allConfigs.at(-1)!; + } + + public readonly directory: string; + + private constructor( + configFile: string, + public readonly allConfigs: string[], + public readonly mergedConfig: BiomeConfigResolved, + public readonly formattedFiles: string[], + ) { + this.directory = path.dirname(configFile); + } + /** + * Create a BiomeConfig instance rooted in the provided directory. + */ + public static async create( + directoryOrConfigFile: string, + gitRepo: GitRepo, + ): Promise { + /** + * The repo root-relative path to the directory being used as the Biome working directory. + */ + let directory: string; + let configFile: string; + if ((await stat(directoryOrConfigFile)).isFile()) { + configFile = directoryOrConfigFile; + directory = path.relative(gitRepo.resolvedRoot, path.dirname(directoryOrConfigFile)); + } else { + configFile = await getClosestBiomeConfigPath(directoryOrConfigFile); + directory = path.relative(gitRepo.resolvedRoot, directoryOrConfigFile); + } + + const allConfigs = await getAllBiomeConfigPaths(configFile); + const mergedConfig = await loadBiomeConfigs(allConfigs); + const files = await getBiomeFormattedFiles(mergedConfig, directory, gitRepo); + return new BiomeConfigReader(configFile, allConfigs, mergedConfig, files); + } +} diff --git a/build-tools/packages/build-tools/src/common/biomeConfigTypes.d.ts b/build-tools/packages/build-tools/src/common/biomeConfigTypes.d.ts new file mode 100644 index 000000000000..0061f19ec401 --- /dev/null +++ b/build-tools/packages/build-tools/src/common/biomeConfigTypes.d.ts @@ -0,0 +1,2105 @@ +/*! + * Copyright (c) Microsoft Corporation and contributors. All rights reserved. + * Licensed under the MIT License. + */ + +/* eslint-disable */ +/** + * This file was automatically generated by json-schema-to-typescript. + * DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file, + * and run json-schema-to-typescript to regenerate this file. + */ + +export type PlainIndentStyle = "tab" | "space"; +export type IndentWidth = number; +export type LineEnding = "lf" | "crlf" | "cr"; +/** + * Validated value for the `line_width` formatter options + * + * The allowed range of values is 1..=320 + */ +export type LineWidth = number; +export type QuoteStyle = "double" | "single"; +export type StringSet = string[]; +export type AttributePosition = "auto" | "multiline"; +export type ArrowParentheses = "always" | "asNeeded"; +export type QuoteProperties = "asNeeded" | "preserve"; +export type Semicolons = "always" | "asNeeded"; +/** + * Print trailing commas wherever possible in multi-line comma-separated syntactic structures. + */ +export type TrailingCommas = "all" | "es5" | "none"; +/** + * Indicates the type of runtime or transformation used for interpreting JSX. + */ +export type JsxRuntime = "transparent" | "reactClassic"; +export type TrailingCommas2 = "none" | "all"; +export type RuleFixConfiguration = RulePlainConfiguration | RuleWithFixNoOptions; +export type RulePlainConfiguration = "warn" | "error" | "info" | "off"; +/** + * Used to identify the kind of code action emitted by a rule + */ +export type FixKind = "none" | "safe" | "unsafe"; +export type RuleConfiguration = RulePlainConfiguration | RuleWithNoOptions; +export type ValidAriaRoleConfiguration = RulePlainConfiguration | RuleWithValidAriaRoleOptions; +export type ComplexityConfiguration = RulePlainConfiguration | RuleWithComplexityOptions; +export type HooksConfiguration = RulePlainConfiguration | RuleWithHooksOptions; +export type StableHookResult = boolean | [number, ...number[]]; +export type DeprecatedHooksConfiguration = + | RulePlainConfiguration + | RuleWithDeprecatedHooksOptions; +export type NoLabelWithoutControlConfiguration = + | RulePlainConfiguration + | RuleWithNoLabelWithoutControlOptions; +export type RestrictedImportsConfiguration = + | RulePlainConfiguration + | RuleWithRestrictedImportsOptions; +export type UtilityClassSortingConfiguration = + | RulePlainConfiguration + | RuleWithUtilityClassSortingOptions; +export type UseValidAutocompleteConfiguration = + | RulePlainConfiguration + | RuleWithUseValidAutocompleteOptions; +export type RestrictedGlobalsConfiguration = + | RulePlainConfiguration + | RuleWithRestrictedGlobalsOptions; +export type ConsistentArrayTypeConfiguration = + | RulePlainConfiguration + | RuleWithConsistentArrayTypeOptions; +export type ConsistentArrayType = "shorthand" | "generic"; +export type FilenamingConventionConfiguration = + | RulePlainConfiguration + | RuleWithFilenamingConventionOptions; +/** + * Supported cases for file names. + */ +export type FilenameCase = "camelCase" | "export" | "kebab-case" | "PascalCase" | "snake_case"; +export type FilenameCases = FilenameCase[]; +export type NamingConventionConfiguration = + | RulePlainConfiguration + | RuleWithNamingConventionOptions; +/** + * Supported cases. + */ +export type Format = "camelCase" | "CONSTANT_CASE" | "PascalCase" | "snake_case"; +export type Formats = Format[]; +export type Regex = string; +export type Kind = + | ( + | "class" + | "enum" + | "interface" + | "enumMember" + | "importNamespace" + | "exportNamespace" + | "variable" + | "const" + | "let" + | "using" + | "var" + | "catchParameter" + | "indexParameter" + | "exportAlias" + | "importAlias" + | "classGetter" + | "classSetter" + | "classMethod" + | "objectLiteralProperty" + | "objectLiteralGetter" + | "objectLiteralSetter" + | "objectLiteralMethod" + | "typeAlias" + ) + | "any" + | "typeLike" + | "function" + | "namespaceLike" + | "namespace" + | "functionParameter" + | "typeParameter" + | "classMember" + | "classProperty" + | "objectLiteralMember" + | "typeMember" + | "typeGetter" + | "typeProperty" + | "typeSetter" + | "typeMethod"; +export type RestrictedModifier = "abstract" | "private" | "protected" | "readonly" | "static"; +export type Modifiers = RestrictedModifier[]; +export type Scope = "any" | "global"; +export type Overrides = OverridePattern[]; +export type VcsClientKind = "git"; + +/** + * The configuration that is contained inside the file `biome.json` + */ +export interface Configuration { + /** + * A field for the [JSON schema](https://json-schema.org/) specification + */ + $schema?: string | null; + /** + * Specific configuration for the Css language + */ + css?: CssConfiguration | null; + /** + * A list of paths to other JSON files, used to extends the current configuration. + */ + extends?: StringSet | null; + /** + * The configuration of the filesystem + */ + files?: FilesConfiguration | null; + /** + * The configuration of the formatter + */ + formatter?: FormatterConfiguration | null; + /** + * Specific configuration for the JavaScript language + */ + javascript?: JavascriptConfiguration | null; + /** + * Specific configuration for the Json language + */ + json?: JsonConfiguration | null; + /** + * The configuration for the linter + */ + linter?: LinterConfiguration | null; + /** + * The configuration of the import sorting + */ + organizeImports?: OrganizeImports | null; + /** + * A list of granular patterns that should be applied only to a sub set of files + */ + overrides?: Overrides | null; + /** + * The configuration of the VCS integration + */ + vcs?: VcsConfiguration | null; +} +/** + * Options applied to CSS files + */ +export interface CssConfiguration { + /** + * CSS formatter options + */ + formatter?: CssFormatter | null; + /** + * CSS linter options + */ + linter?: CssLinter | null; + /** + * CSS parsing options + */ + parser?: CssParser | null; +} +/** + * Options that changes how the CSS formatter behaves + */ +export interface CssFormatter { + /** + * Control the formatter for CSS (and its super languages) files. + */ + enabled?: boolean | null; + /** + * The indent style applied to CSS (and its super languages) files. + */ + indentStyle?: PlainIndentStyle | null; + /** + * The size of the indentation applied to CSS (and its super languages) files. Default to 2. + */ + indentWidth?: IndentWidth | null; + /** + * The type of line ending applied to CSS (and its super languages) files. + */ + lineEnding?: LineEnding | null; + /** + * What's the max width of a line applied to CSS (and its super languages) files. Defaults to 80. + */ + lineWidth?: LineWidth | null; + /** + * The type of quotes used in CSS code. Defaults to double. + */ + quoteStyle?: QuoteStyle | null; +} +/** + * Options that changes how the CSS linter behaves + */ +export interface CssLinter { + /** + * Control the linter for CSS (and its super languages) files. + */ + enabled?: boolean | null; +} +/** + * Options that changes how the CSS parser behaves + */ +export interface CssParser { + /** + * Allow comments to appear on incorrect lines in `.css` files + */ + allowWrongLineComments?: boolean | null; + /** + * Enables parsing of CSS Modules specific features. + */ + cssModules?: boolean | null; +} +/** + * The configuration of the filesystem + */ +export interface FilesConfiguration { + /** + * A list of Unix shell style patterns. Biome will ignore files/folders that will match these patterns. + */ + ignore?: StringSet | null; + /** + * Tells Biome to not emit diagnostics when handling files that doesn't know + */ + ignoreUnknown?: boolean | null; + /** + * A list of Unix shell style patterns. Biome will handle only those files/folders that will match these patterns. + */ + include?: StringSet | null; + /** + * The maximum allowed size for source code files in bytes. Files above this limit will be ignored for performance reasons. Defaults to 1 MiB + */ + maxSize?: number | null; +} +/** + * Generic options applied to all files + */ +export interface FormatterConfiguration { + /** + * The attribute position style in HTMLish languages. By default auto. + */ + attributePosition?: AttributePosition | null; + enabled?: boolean | null; + /** + * Stores whether formatting should be allowed to proceed if a given file has syntax errors + */ + formatWithErrors?: boolean | null; + /** + * A list of Unix shell style patterns. The formatter will ignore files/folders that will match these patterns. + */ + ignore?: StringSet | null; + /** + * A list of Unix shell style patterns. The formatter will include files/folders that will match these patterns. + */ + include?: StringSet | null; + /** + * The size of the indentation, 2 by default (deprecated, use `indent-width`) + */ + indentSize?: IndentWidth | null; + /** + * The indent style. + */ + indentStyle?: PlainIndentStyle | null; + /** + * The size of the indentation, 2 by default + */ + indentWidth?: IndentWidth | null; + /** + * The type of line ending. + */ + lineEnding?: LineEnding | null; + /** + * What's the max width of a line. Defaults to 80. + */ + lineWidth?: LineWidth | null; +} +/** + * A set of options applied to the JavaScript files + */ +export interface JavascriptConfiguration { + /** + * Formatting options + */ + formatter?: JavascriptFormatter | null; + /** + * A list of global bindings that should be ignored by the analyzers + * + * If defined here, they should not emit diagnostics. + */ + globals?: StringSet | null; + /** + * Indicates the type of runtime or transformation used for interpreting JSX. + */ + jsxRuntime?: JsxRuntime | null; + /** + * Linter options + */ + linter?: JavascriptLinter | null; + organizeImports?: JavascriptOrganizeImports | null; + /** + * Parsing options + */ + parser?: JavascriptParser | null; +} +/** + * Formatting options specific to the JavaScript files + */ +export interface JavascriptFormatter { + /** + * Whether to add non-necessary parentheses to arrow functions. Defaults to "always". + */ + arrowParentheses?: ArrowParentheses | null; + /** + * The attribute position style in jsx elements. Defaults to auto. + */ + attributePosition?: AttributePosition | null; + /** + * Whether to hug the closing bracket of multiline HTML/JSX tags to the end of the last line, rather than being alone on the following line. Defaults to false. + */ + bracketSameLine?: boolean | null; + /** + * Whether to insert spaces around brackets in object literals. Defaults to true. + */ + bracketSpacing?: boolean | null; + /** + * Control the formatter for JavaScript (and its super languages) files. + */ + enabled?: boolean | null; + /** + * The size of the indentation applied to JavaScript (and its super languages) files. Default to 2. + */ + indentSize?: IndentWidth | null; + /** + * The indent style applied to JavaScript (and its super languages) files. + */ + indentStyle?: PlainIndentStyle | null; + /** + * The size of the indentation applied to JavaScript (and its super languages) files. Default to 2. + */ + indentWidth?: IndentWidth | null; + /** + * The type of quotes used in JSX. Defaults to double. + */ + jsxQuoteStyle?: QuoteStyle | null; + /** + * The type of line ending applied to JavaScript (and its super languages) files. + */ + lineEnding?: LineEnding | null; + /** + * What's the max width of a line applied to JavaScript (and its super languages) files. Defaults to 80. + */ + lineWidth?: LineWidth | null; + /** + * When properties in objects are quoted. Defaults to asNeeded. + */ + quoteProperties?: QuoteProperties | null; + /** + * The type of quotes used in JavaScript code. Defaults to double. + */ + quoteStyle?: QuoteStyle | null; + /** + * Whether the formatter prints semicolons for all statements or only in for statements where it is necessary because of ASI. + */ + semicolons?: Semicolons | null; + /** + * Print trailing commas wherever possible in multi-line comma-separated syntactic structures. Defaults to "all". + */ + trailingComma?: TrailingCommas | null; + /** + * Print trailing commas wherever possible in multi-line comma-separated syntactic structures. Defaults to "all". + */ + trailingCommas?: TrailingCommas | null; +} +/** + * Linter options specific to the JavaScript linter + */ +export interface JavascriptLinter { + /** + * Control the linter for JavaScript (and its super languages) files. + */ + enabled?: boolean | null; +} +export interface JavascriptOrganizeImports {} +/** + * Options that changes how the JavaScript parser behaves + */ +export interface JavascriptParser { + /** + * It enables the experimental and unsafe parsing of parameter decorators + * + * These decorators belong to an old proposal, and they are subject to change. + */ + unsafeParameterDecoratorsEnabled?: boolean | null; +} +/** + * Options applied to JSON files + */ +export interface JsonConfiguration { + /** + * Formatting options + */ + formatter?: JsonFormatter | null; + /** + * Linting options + */ + linter?: JsonLinter | null; + /** + * Parsing options + */ + parser?: JsonParser | null; +} +export interface JsonFormatter { + /** + * Control the formatter for JSON (and its super languages) files. + */ + enabled?: boolean | null; + /** + * The size of the indentation applied to JSON (and its super languages) files. Default to 2. + */ + indentSize?: IndentWidth | null; + /** + * The indent style applied to JSON (and its super languages) files. + */ + indentStyle?: PlainIndentStyle | null; + /** + * The size of the indentation applied to JSON (and its super languages) files. Default to 2. + */ + indentWidth?: IndentWidth | null; + /** + * The type of line ending applied to JSON (and its super languages) files. + */ + lineEnding?: LineEnding | null; + /** + * What's the max width of a line applied to JSON (and its super languages) files. Defaults to 80. + */ + lineWidth?: LineWidth | null; + /** + * Print trailing commas wherever possible in multi-line comma-separated syntactic structures. Defaults to "none". + */ + trailingCommas?: TrailingCommas2 | null; +} +/** + * Linter options specific to the JSON linter + */ +export interface JsonLinter { + /** + * Control the linter for JSON (and its super languages) files. + */ + enabled?: boolean | null; +} +/** + * Options that changes how the JSON parser behaves + */ +export interface JsonParser { + /** + * Allow parsing comments in `.json` files + */ + allowComments?: boolean | null; + /** + * Allow parsing trailing commas in `.json` files + */ + allowTrailingCommas?: boolean | null; +} +export interface LinterConfiguration { + /** + * if `false`, it disables the feature and the linter won't be executed. `true` by default + */ + enabled?: boolean | null; + /** + * A list of Unix shell style patterns. The formatter will ignore files/folders that will match these patterns. + */ + ignore?: StringSet | null; + /** + * A list of Unix shell style patterns. The formatter will include files/folders that will match these patterns. + */ + include?: StringSet | null; + /** + * List of rules + */ + rules?: Rules | null; +} +export interface Rules { + a11y?: A11Y | null; + /** + * It enables ALL rules. The rules that belong to `nursery` won't be enabled. + */ + all?: boolean | null; + complexity?: Complexity | null; + correctness?: Correctness | null; + nursery?: Nursery | null; + performance?: Performance | null; + /** + * It enables the lint rules recommended by Biome. `true` by default. + */ + recommended?: boolean | null; + security?: Security | null; + style?: Style | null; + suspicious?: Suspicious | null; +} +/** + * A list of rules that belong to this group + */ +export interface A11Y { + /** + * It enables ALL rules for this group. + */ + all?: boolean | null; + /** + * Enforce that the accessKey attribute is not used on any HTML element. + */ + noAccessKey?: RuleFixConfiguration | null; + /** + * Enforce that aria-hidden="true" is not set on focusable elements. + */ + noAriaHiddenOnFocusable?: RuleFixConfiguration | null; + /** + * Enforce that elements that do not support ARIA roles, states, and properties do not have those attributes. + */ + noAriaUnsupportedElements?: RuleFixConfiguration | null; + /** + * Enforce that autoFocus prop is not used on elements. + */ + noAutofocus?: RuleFixConfiguration | null; + /** + * Disallow target="_blank" attribute without rel="noreferrer" + */ + noBlankTarget?: RuleFixConfiguration | null; + /** + * Enforces that no distracting elements are used. + */ + noDistractingElements?: RuleFixConfiguration | null; + /** + * The scope prop should be used only on \ elements. + */ + noHeaderScope?: RuleFixConfiguration | null; + /** + * Enforce that non-interactive ARIA roles are not assigned to interactive HTML elements. + */ + noInteractiveElementToNoninteractiveRole?: RuleFixConfiguration | null; + /** + * Enforce that interactive ARIA roles are not assigned to non-interactive HTML elements. + */ + noNoninteractiveElementToInteractiveRole?: RuleFixConfiguration | null; + /** + * Enforce that tabIndex is not assigned to non-interactive HTML elements. + */ + noNoninteractiveTabindex?: RuleFixConfiguration | null; + /** + * Prevent the usage of positive integers on tabIndex property + */ + noPositiveTabindex?: RuleFixConfiguration | null; + /** + * Enforce img alt prop does not contain the word "image", "picture", or "photo". + */ + noRedundantAlt?: RuleConfiguration | null; + /** + * Enforce explicit role property is not the same as implicit/default role property on an element. + */ + noRedundantRoles?: RuleFixConfiguration | null; + /** + * Enforces the usage of the title element for the svg element. + */ + noSvgWithoutTitle?: RuleConfiguration | null; + /** + * It enables the recommended rules for this group + */ + recommended?: boolean | null; + /** + * Enforce that all elements that require alternative text have meaningful information to relay back to the end user. + */ + useAltText?: RuleConfiguration | null; + /** + * Enforce that anchors have content and that the content is accessible to screen readers. + */ + useAnchorContent?: RuleFixConfiguration | null; + /** + * Enforce that tabIndex is assigned to non-interactive HTML elements with aria-activedescendant. + */ + useAriaActivedescendantWithTabindex?: RuleFixConfiguration | null; + /** + * Enforce that elements with ARIA roles must have all required ARIA attributes for that role. + */ + useAriaPropsForRole?: RuleConfiguration | null; + /** + * Enforces the usage of the attribute type for the element button + */ + useButtonType?: RuleConfiguration | null; + /** + * Enforce that heading elements (h1, h2, etc.) have content and that the content is accessible to screen readers. Accessible means that it is not hidden using the aria-hidden prop. + */ + useHeadingContent?: RuleConfiguration | null; + /** + * Enforce that html element has lang attribute. + */ + useHtmlLang?: RuleConfiguration | null; + /** + * Enforces the usage of the attribute title for the element iframe. + */ + useIframeTitle?: RuleConfiguration | null; + /** + * Enforce onClick is accompanied by at least one of the following: onKeyUp, onKeyDown, onKeyPress. + */ + useKeyWithClickEvents?: RuleConfiguration | null; + /** + * Enforce onMouseOver / onMouseOut are accompanied by onFocus / onBlur. + */ + useKeyWithMouseEvents?: RuleConfiguration | null; + /** + * Enforces that audio and video elements must have a track for captions. + */ + useMediaCaption?: RuleConfiguration | null; + /** + * Enforce that all anchors are valid, and they are navigable elements. + */ + useValidAnchor?: RuleConfiguration | null; + /** + * Ensures that ARIA properties aria-* are all valid. + */ + useValidAriaProps?: RuleFixConfiguration | null; + /** + * Elements with ARIA roles must use a valid, non-abstract ARIA role. + */ + useValidAriaRole?: ValidAriaRoleConfiguration | null; + /** + * Enforce that ARIA state and property values are valid. + */ + useValidAriaValues?: RuleConfiguration | null; + /** + * Ensure that the attribute passed to the lang attribute is a correct ISO language and/or country. + */ + useValidLang?: RuleConfiguration | null; +} +export interface RuleWithFixNoOptions { + /** + * The kind of the code actions emitted by the rule + */ + fix?: FixKind | null; + /** + * The severity of the emitted diagnostics by the rule + */ + level: RulePlainConfiguration; +} +export interface RuleWithNoOptions { + /** + * The severity of the emitted diagnostics by the rule + */ + level: RulePlainConfiguration; +} +export interface RuleWithValidAriaRoleOptions { + /** + * The kind of the code actions emitted by the rule + */ + fix?: FixKind | null; + /** + * The severity of the emitted diagnostics by the rule + */ + level: RulePlainConfiguration; + /** + * Rule's options + */ + options: ValidAriaRoleOptions; +} +export interface ValidAriaRoleOptions { + allowInvalidRoles: string[]; + ignoreNonDom: boolean; +} +/** + * A list of rules that belong to this group + */ +export interface Complexity { + /** + * It enables ALL rules for this group. + */ + all?: boolean | null; + /** + * Disallow primitive type aliases and misleading types. + */ + noBannedTypes?: RuleFixConfiguration | null; + /** + * Disallow empty type parameters in type aliases and interfaces. + */ + noEmptyTypeParameters?: RuleConfiguration | null; + /** + * Disallow functions that exceed a given Cognitive Complexity score. + */ + noExcessiveCognitiveComplexity?: ComplexityConfiguration | null; + /** + * This rule enforces a maximum depth to nested describe() in test files. + */ + noExcessiveNestedTestSuites?: RuleConfiguration | null; + /** + * Disallow unnecessary boolean casts + */ + noExtraBooleanCast?: RuleFixConfiguration | null; + /** + * Prefer for...of statement instead of Array.forEach. + */ + noForEach?: RuleConfiguration | null; + /** + * Disallow unclear usage of consecutive space characters in regular expression literals + */ + noMultipleSpacesInRegularExpressionLiterals?: RuleFixConfiguration | null; + /** + * This rule reports when a class has no non-static members, such as for a class used exclusively as a static namespace. + */ + noStaticOnlyClass?: RuleConfiguration | null; + /** + * Disallow this and super in static contexts. + */ + noThisInStatic?: RuleFixConfiguration | null; + /** + * Disallow unnecessary catch clauses. + */ + noUselessCatch?: RuleConfiguration | null; + /** + * Disallow unnecessary constructors. + */ + noUselessConstructor?: RuleFixConfiguration | null; + /** + * Disallow empty exports that don't change anything in a module file. + */ + noUselessEmptyExport?: RuleFixConfiguration | null; + /** + * Disallow unnecessary fragments + */ + noUselessFragments?: RuleFixConfiguration | null; + /** + * Disallow unnecessary labels. + */ + noUselessLabel?: RuleFixConfiguration | null; + /** + * Disallow unnecessary nested block statements. + */ + noUselessLoneBlockStatements?: RuleFixConfiguration | null; + /** + * Disallow renaming import, export, and destructured assignments to the same name. + */ + noUselessRename?: RuleFixConfiguration | null; + /** + * Disallow useless case in switch statements. + */ + noUselessSwitchCase?: RuleFixConfiguration | null; + /** + * Disallow ternary operators when simpler alternatives exist. + */ + noUselessTernary?: RuleFixConfiguration | null; + /** + * Disallow useless this aliasing. + */ + noUselessThisAlias?: RuleFixConfiguration | null; + /** + * Disallow using any or unknown as type constraint. + */ + noUselessTypeConstraint?: RuleFixConfiguration | null; + /** + * Disallow the use of void operators, which is not a familiar operator. + */ + noVoid?: RuleConfiguration | null; + /** + * Disallow with statements in non-strict contexts. + */ + noWith?: RuleConfiguration | null; + /** + * It enables the recommended rules for this group + */ + recommended?: boolean | null; + /** + * Use arrow functions over function expressions. + */ + useArrowFunction?: RuleFixConfiguration | null; + /** + * Promotes the use of .flatMap() when map().flat() are used together. + */ + useFlatMap?: RuleFixConfiguration | null; + /** + * Enforce the usage of a literal access to properties over computed property access. + */ + useLiteralKeys?: RuleFixConfiguration | null; + /** + * Enforce using concise optional chain instead of chained logical expressions. + */ + useOptionalChain?: RuleFixConfiguration | null; + /** + * Enforce the use of the regular expression literals instead of the RegExp constructor if possible. + */ + useRegexLiterals?: RuleFixConfiguration | null; + /** + * Disallow number literal object member names which are not base10 or uses underscore as separator + */ + useSimpleNumberKeys?: RuleFixConfiguration | null; + /** + * Discard redundant terms from logical expressions. + */ + useSimplifiedLogicExpression?: RuleFixConfiguration | null; +} +export interface RuleWithComplexityOptions { + /** + * The severity of the emitted diagnostics by the rule + */ + level: RulePlainConfiguration; + /** + * Rule's options + */ + options: ComplexityOptions; +} +/** + * Options for the rule `noExcessiveCognitiveComplexity`. + */ +export interface ComplexityOptions { + /** + * The maximum complexity score that we allow. Anything higher is considered excessive. + */ + maxAllowedComplexity: number; +} +/** + * A list of rules that belong to this group + */ +export interface Correctness { + /** + * It enables ALL rules for this group. + */ + all?: boolean | null; + /** + * Prevent passing of children as props. + */ + noChildrenProp?: RuleConfiguration | null; + /** + * Prevents from having const variables being re-assigned. + */ + noConstAssign?: RuleFixConfiguration | null; + /** + * Disallow constant expressions in conditions + */ + noConstantCondition?: RuleConfiguration | null; + /** + * Disallow the use of Math.min and Math.max to clamp a value where the result itself is constant. + */ + noConstantMathMinMaxClamp?: RuleFixConfiguration | null; + /** + * Disallow returning a value from a constructor. + */ + noConstructorReturn?: RuleConfiguration | null; + /** + * Disallow empty character classes in regular expression literals. + */ + noEmptyCharacterClassInRegex?: RuleConfiguration | null; + /** + * Disallows empty destructuring patterns. + */ + noEmptyPattern?: RuleConfiguration | null; + /** + * Disallow to use unnecessary callback on flatMap. + */ + noFlatMapIdentity?: RuleFixConfiguration | null; + /** + * Disallow calling global object properties as functions + */ + noGlobalObjectCalls?: RuleConfiguration | null; + /** + * Disallow function and var declarations that are accessible outside their block. + */ + noInnerDeclarations?: RuleConfiguration | null; + /** + * Prevents the incorrect use of super() inside classes. It also checks whether a call super() is missing from classes that extends other constructors. + */ + noInvalidConstructorSuper?: RuleConfiguration | null; + /** + * Disallow new operators with global non-constructor functions. + */ + noInvalidNewBuiltin?: RuleFixConfiguration | null; + /** + * Disallow the use of variables and function parameters before their declaration + */ + noInvalidUseBeforeDeclaration?: RuleConfiguration | null; + /** + * Disallow new operators with the Symbol object. + */ + noNewSymbol?: RuleFixConfiguration | null; + /** + * Forbid the use of Node.js builtin modules. + */ + noNodejsModules?: RuleConfiguration | null; + /** + * Disallow \8 and \9 escape sequences in string literals. + */ + noNonoctalDecimalEscape?: RuleFixConfiguration | null; + /** + * Disallow literal numbers that lose precision + */ + noPrecisionLoss?: RuleConfiguration | null; + /** + * Prevent the usage of the return value of React.render. + */ + noRenderReturnValue?: RuleConfiguration | null; + /** + * Disallow assignments where both sides are exactly the same. + */ + noSelfAssign?: RuleConfiguration | null; + /** + * Disallow returning a value from a setter + */ + noSetterReturn?: RuleConfiguration | null; + /** + * Disallow comparison of expressions modifying the string case with non-compliant value. + */ + noStringCaseMismatch?: RuleFixConfiguration | null; + /** + * Disallow lexical declarations in switch clauses. + */ + noSwitchDeclarations?: RuleFixConfiguration | null; + /** + * Prevents the usage of variables that haven't been declared inside the document. + */ + noUndeclaredVariables?: RuleConfiguration | null; + /** + * Avoid using unnecessary continue. + */ + noUnnecessaryContinue?: RuleFixConfiguration | null; + /** + * Disallow unreachable code + */ + noUnreachable?: RuleConfiguration | null; + /** + * Ensures the super() constructor is called exactly once on every code path in a class constructor before this is accessed if the class has a superclass + */ + noUnreachableSuper?: RuleConfiguration | null; + /** + * Disallow control flow statements in finally blocks. + */ + noUnsafeFinally?: RuleConfiguration | null; + /** + * Disallow the use of optional chaining in contexts where the undefined value is not allowed. + */ + noUnsafeOptionalChaining?: RuleConfiguration | null; + /** + * Disallow unused imports. + */ + noUnusedImports?: RuleFixConfiguration | null; + /** + * Disallow unused labels. + */ + noUnusedLabels?: RuleFixConfiguration | null; + /** + * Disallow unused private class members + */ + noUnusedPrivateClassMembers?: RuleFixConfiguration | null; + /** + * Disallow unused variables. + */ + noUnusedVariables?: RuleFixConfiguration | null; + /** + * This rules prevents void elements (AKA self-closing elements) from having children. + */ + noVoidElementsWithChildren?: RuleFixConfiguration | null; + /** + * Disallow returning a value from a function with the return type 'void' + */ + noVoidTypeReturn?: RuleConfiguration | null; + /** + * It enables the recommended rules for this group + */ + recommended?: boolean | null; + /** + * Disallow Array constructors. + */ + useArrayLiterals?: RuleFixConfiguration | null; + /** + * Enforce all dependencies are correctly specified in a React hook. + */ + useExhaustiveDependencies?: HooksConfiguration | null; + /** + * Enforce that all React hooks are being called from the Top Level component functions. + */ + useHookAtTopLevel?: DeprecatedHooksConfiguration | null; + /** + * Require calls to isNaN() when checking for NaN. + */ + useIsNan?: RuleFixConfiguration | null; + /** + * Disallow missing key props in iterators/collection literals. + */ + useJsxKeyInIterable?: RuleConfiguration | null; + /** + * Enforce "for" loop update clause moving the counter in the right direction. + */ + useValidForDirection?: RuleConfiguration | null; + /** + * Require generator functions to contain yield. + */ + useYield?: RuleConfiguration | null; +} +export interface RuleWithHooksOptions { + /** + * The severity of the emitted diagnostics by the rule + */ + level: RulePlainConfiguration; + /** + * Rule's options + */ + options: HooksOptions; +} +/** + * Options for the rule `useExhaustiveDependencies` + */ +export interface HooksOptions { + /** + * List of hooks of which the dependencies should be validated. + */ + hooks: Hook[]; +} +export interface Hook { + /** + * The "position" of the closure function, starting from zero. + * + * For example, for React's `useEffect()` hook, the closure index is 0. + */ + closureIndex?: number | null; + /** + * The "position" of the array of dependencies, starting from zero. + * + * For example, for React's `useEffect()` hook, the dependencies index is 1. + */ + dependenciesIndex?: number | null; + /** + * The name of the hook. + */ + name: string; + /** + * Whether the result of the hook is stable. + * + * Set to `true` to mark the identity of the hook's return value as stable, or use a number/an array of numbers to mark the "positions" in the return array as stable. + * + * For example, for React's `useRef()` hook the value would be `true`, while for `useState()` it would be `[1]`. + */ + stableResult: StableHookResult; +} +export interface RuleWithDeprecatedHooksOptions { + /** + * The severity of the emitted diagnostics by the rule + */ + level: RulePlainConfiguration; + /** + * Rule's options + */ + options: DeprecatedHooksOptions; +} +/** + * Options for the `useHookAtTopLevel` rule have been deprecated, since we now use the React hook naming convention to determine whether a function is a hook. + */ +export interface DeprecatedHooksOptions {} +/** + * A list of rules that belong to this group + */ +export interface Nursery { + /** + * It enables ALL rules for this group. + */ + all?: boolean | null; + /** + * Disallow the use of console. + */ + noConsole?: RuleFixConfiguration | null; + /** + * Disallow using a callback in asynchronous tests and hooks. + */ + noDoneCallback?: RuleConfiguration | null; + /** + * Disallow duplicate @import rules. + */ + noDuplicateAtImportRules?: RuleConfiguration | null; + /** + * Disallow duplicate conditions in if-else-if chains + */ + noDuplicateElseIf?: RuleConfiguration | null; + /** + * Disallow duplicate names within font families. + */ + noDuplicateFontNames?: RuleConfiguration | null; + /** + * Disallow two keys with the same name inside a JSON object. + */ + noDuplicateJsonKeys?: RuleConfiguration | null; + /** + * Disallow duplicate selectors within keyframe blocks. + */ + noDuplicateSelectorsKeyframeBlock?: RuleConfiguration | null; + /** + * Disallow CSS empty blocks. + */ + noEmptyBlock?: RuleConfiguration | null; + /** + * Disallow variables from evolving into any type through reassignments. + */ + noEvolvingTypes?: RuleConfiguration | null; + /** + * Disallow exporting an imported variable. + */ + noExportedImports?: RuleConfiguration | null; + /** + * Disallow invalid !important within keyframe declarations + */ + noImportantInKeyframe?: RuleConfiguration | null; + /** + * Disallow non-standard direction values for linear gradient functions. + */ + noInvalidDirectionInLinearGradient?: RuleConfiguration | null; + /** + * Disallow the use of @import at-rules in invalid positions. + */ + noInvalidPositionAtImportRule?: RuleConfiguration | null; + /** + * Enforce that a label element or component has a text label and an associated input. + */ + noLabelWithoutControl?: NoLabelWithoutControlConfiguration | null; + /** + * Checks that the assertion function, for example expect, is placed inside an it() function call. + */ + noMisplacedAssertion?: RuleConfiguration | null; + /** + * Prevents React-specific JSX properties from being used. + */ + noReactSpecificProps?: RuleFixConfiguration | null; + /** + * Disallow specified modules when loaded by import or require. + */ + noRestrictedImports?: RestrictedImportsConfiguration | null; + /** + * Disallow shorthand properties that override related longhand properties. + */ + noShorthandPropertyOverrides?: RuleConfiguration | null; + /** + * Enforce the use of String.slice() over String.substr() and String.substring(). + */ + noSubstr?: RuleFixConfiguration | null; + /** + * Disallow the use of dependencies that aren't specified in the package.json. + */ + noUndeclaredDependencies?: RuleConfiguration | null; + /** + * Disallow unknown CSS value functions. + */ + noUnknownFunction?: RuleConfiguration | null; + /** + * Disallow unknown media feature names. + */ + noUnknownMediaFeatureName?: RuleConfiguration | null; + /** + * Disallow unknown properties. + */ + noUnknownProperty?: RuleConfiguration | null; + /** + * Disallow unknown pseudo-class selectors. + */ + noUnknownPseudoClassSelector?: RuleConfiguration | null; + /** + * Disallow unknown pseudo-element selectors. + */ + noUnknownSelectorPseudoElement?: RuleConfiguration | null; + /** + * Disallow unknown CSS units. + */ + noUnknownUnit?: RuleConfiguration | null; + /** + * Disallow unmatchable An+B selectors. + */ + noUnmatchableAnbSelector?: RuleConfiguration | null; + /** + * Disallow unused function parameters. + */ + noUnusedFunctionParameters?: RuleFixConfiguration | null; + /** + * Disallow unnecessary concatenation of string or template literals. + */ + noUselessStringConcat?: RuleFixConfiguration | null; + /** + * Disallow initializing variables to undefined. + */ + noUselessUndefinedInitialization?: RuleFixConfiguration | null; + /** + * Disallow the use of yoda expressions. + */ + noYodaExpression?: RuleFixConfiguration | null; + /** + * It enables the recommended rules for this group + */ + recommended?: boolean | null; + /** + * Disallow the use of overload signatures that are not next to each other. + */ + useAdjacentOverloadSignatures?: RuleConfiguration | null; + /** + * Enforce the use of new for all builtins, except String, Number, Boolean, Symbol and BigInt. + */ + useConsistentBuiltinInstantiation?: RuleFixConfiguration | null; + /** + * Disallows invalid named grid areas in CSS Grid Layouts. + */ + useConsistentGridAreas?: RuleConfiguration | null; + /** + * Use Date.now() to get the number of milliseconds since the Unix Epoch. + */ + useDateNow?: RuleFixConfiguration | null; + /** + * Require the default clause in switch statements. + */ + useDefaultSwitchClause?: RuleConfiguration | null; + /** + * Require specifying the reason argument when using @deprecated directive + */ + useDeprecatedReason?: RuleConfiguration | null; + /** + * Enforce passing a message value when creating a built-in error. + */ + useErrorMessage?: RuleConfiguration | null; + /** + * Enforce explicitly comparing the length, size, byteLength or byteOffset property of a value. + */ + useExplicitLengthCheck?: RuleFixConfiguration | null; + /** + * Elements with an interactive role and interaction handlers must be focusable. + */ + useFocusableInteractive?: RuleConfiguration | null; + /** + * Disallow a missing generic family keyword within font families. + */ + useGenericFontNames?: RuleConfiguration | null; + /** + * Enforce file extensions for relative imports. + */ + useImportExtensions?: RuleFixConfiguration | null; + /** + * Disallows package private imports. + */ + useImportRestrictions?: RuleConfiguration | null; + /** + * Enforce using the digits argument with Number#toFixed(). + */ + useNumberToFixedDigitsArgument?: RuleFixConfiguration | null; + /** + * It detects the use of role attributes in JSX elements and suggests using semantic elements instead. + */ + useSemanticElements?: RuleConfiguration | null; + /** + * Enforce the sorting of CSS utility classes. + */ + useSortedClasses?: UtilityClassSortingConfiguration | null; + /** + * Require new when throwing an error. + */ + useThrowNewError?: RuleFixConfiguration | null; + /** + * Disallow throwing non-Error values. + */ + useThrowOnlyError?: RuleConfiguration | null; + /** + * Require regex literals to be declared at the top level. + */ + useTopLevelRegex?: RuleConfiguration | null; + /** + * Use valid values for the autocomplete attribute on input elements. + */ + useValidAutocomplete?: UseValidAutocompleteConfiguration | null; +} +export interface RuleWithNoLabelWithoutControlOptions { + /** + * The severity of the emitted diagnostics by the rule + */ + level: RulePlainConfiguration; + /** + * Rule's options + */ + options: NoLabelWithoutControlOptions; +} +export interface NoLabelWithoutControlOptions { + /** + * Array of component names that should be considered the same as an `input` element. + */ + inputComponents: string[]; + /** + * Array of attributes that should be treated as the `label` accessible text content. + */ + labelAttributes: string[]; + /** + * Array of component names that should be considered the same as a `label` element. + */ + labelComponents: string[]; +} +export interface RuleWithRestrictedImportsOptions { + /** + * The severity of the emitted diagnostics by the rule + */ + level: RulePlainConfiguration; + /** + * Rule's options + */ + options: RestrictedImportsOptions; +} +/** + * Options for the rule `noRestrictedImports`. + */ +export interface RestrictedImportsOptions { + /** + * A list of names that should trigger the rule + */ + paths: { + [k: string]: string; + }; +} +export interface RuleWithUtilityClassSortingOptions { + /** + * The kind of the code actions emitted by the rule + */ + fix?: FixKind | null; + /** + * The severity of the emitted diagnostics by the rule + */ + level: RulePlainConfiguration; + /** + * Rule's options + */ + options: UtilityClassSortingOptions; +} +export interface UtilityClassSortingOptions { + /** + * Additional attributes that will be sorted. + */ + attributes?: string[] | null; + /** + * Names of the functions or tagged templates that will be sorted. + */ + functions?: string[] | null; +} +export interface RuleWithUseValidAutocompleteOptions { + /** + * The severity of the emitted diagnostics by the rule + */ + level: RulePlainConfiguration; + /** + * Rule's options + */ + options: UseValidAutocompleteOptions; +} +export interface UseValidAutocompleteOptions { + /** + * `input` like custom components that should be checked. + */ + inputComponents: string[]; +} +/** + * A list of rules that belong to this group + */ +export interface Performance { + /** + * It enables ALL rules for this group. + */ + all?: boolean | null; + /** + * Disallow the use of spread (...) syntax on accumulators. + */ + noAccumulatingSpread?: RuleConfiguration | null; + /** + * Disallow the use of barrel file. + */ + noBarrelFile?: RuleConfiguration | null; + /** + * Disallow the use of the delete operator. + */ + noDelete?: RuleFixConfiguration | null; + /** + * Avoid re-export all. + */ + noReExportAll?: RuleConfiguration | null; + /** + * It enables the recommended rules for this group + */ + recommended?: boolean | null; +} +/** + * A list of rules that belong to this group + */ +export interface Security { + /** + * It enables ALL rules for this group. + */ + all?: boolean | null; + /** + * Prevent the usage of dangerous JSX props + */ + noDangerouslySetInnerHtml?: RuleConfiguration | null; + /** + * Report when a DOM element or a component uses both children and dangerouslySetInnerHTML prop. + */ + noDangerouslySetInnerHtmlWithChildren?: RuleConfiguration | null; + /** + * Disallow the use of global eval(). + */ + noGlobalEval?: RuleConfiguration | null; + /** + * It enables the recommended rules for this group + */ + recommended?: boolean | null; +} +/** + * A list of rules that belong to this group + */ +export interface Style { + /** + * It enables ALL rules for this group. + */ + all?: boolean | null; + /** + * Disallow the use of arguments. + */ + noArguments?: RuleConfiguration | null; + /** + * Disallow comma operator. + */ + noCommaOperator?: RuleConfiguration | null; + /** + * Disallow default exports. + */ + noDefaultExport?: RuleConfiguration | null; + /** + * Disallow implicit true values on JSX boolean attributes + */ + noImplicitBoolean?: RuleFixConfiguration | null; + /** + * Disallow type annotations for variables, parameters, and class properties initialized with a literal expression. + */ + noInferrableTypes?: RuleFixConfiguration | null; + /** + * Disallow the use of TypeScript's namespaces. + */ + noNamespace?: RuleConfiguration | null; + /** + * Disallow the use of namespace imports. + */ + noNamespaceImport?: RuleConfiguration | null; + /** + * Disallow negation in the condition of an if statement if it has an else clause. + */ + noNegationElse?: RuleFixConfiguration | null; + /** + * Disallow non-null assertions using the ! postfix operator. + */ + noNonNullAssertion?: RuleFixConfiguration | null; + /** + * Disallow reassigning function parameters. + */ + noParameterAssign?: RuleConfiguration | null; + /** + * Disallow the use of parameter properties in class constructors. + */ + noParameterProperties?: RuleConfiguration | null; + /** + * This rule allows you to specify global variable names that you don’t want to use in your application. + */ + noRestrictedGlobals?: RestrictedGlobalsConfiguration | null; + /** + * Disallow the use of constants which its value is the upper-case version of its name. + */ + noShoutyConstants?: RuleFixConfiguration | null; + /** + * Disallow template literals if interpolation and special-character handling are not needed + */ + noUnusedTemplateLiteral?: RuleFixConfiguration | null; + /** + * Disallow else block when the if block breaks early. + */ + noUselessElse?: RuleFixConfiguration | null; + /** + * Disallow the use of var + */ + noVar?: RuleFixConfiguration | null; + /** + * It enables the recommended rules for this group + */ + recommended?: boolean | null; + /** + * Enforce the use of as const over literal type and type annotation. + */ + useAsConstAssertion?: RuleFixConfiguration | null; + /** + * Requires following curly brace conventions. + */ + useBlockStatements?: RuleFixConfiguration | null; + /** + * Enforce using else if instead of nested if in else clauses. + */ + useCollapsedElseIf?: RuleFixConfiguration | null; + /** + * Require consistently using either T\[] or Array\ + */ + useConsistentArrayType?: ConsistentArrayTypeConfiguration | null; + /** + * Require const declarations for variables that are only assigned once. + */ + useConst?: RuleFixConfiguration | null; + /** + * Enforce default function parameters and optional function parameters to be last. + */ + useDefaultParameterLast?: RuleFixConfiguration | null; + /** + * Require that each enum member value be explicitly initialized. + */ + useEnumInitializers?: RuleFixConfiguration | null; + /** + * Disallow the use of Math.pow in favor of the ** operator. + */ + useExponentiationOperator?: RuleFixConfiguration | null; + /** + * Promotes the use of export type for types. + */ + useExportType?: RuleFixConfiguration | null; + /** + * Enforce naming conventions for JavaScript and TypeScript filenames. + */ + useFilenamingConvention?: FilenamingConventionConfiguration | null; + /** + * This rule recommends a for-of loop when in a for loop, the index used to extract an item from the iterated array. + */ + useForOf?: RuleConfiguration | null; + /** + * This rule enforces the use of \<>...\ over \...\. + */ + useFragmentSyntax?: RuleFixConfiguration | null; + /** + * Promotes the use of import type for types. + */ + useImportType?: RuleFixConfiguration | null; + /** + * Require all enum members to be literal values. + */ + useLiteralEnumMembers?: RuleConfiguration | null; + /** + * Enforce naming conventions for everything across a codebase. + */ + useNamingConvention?: NamingConventionConfiguration | null; + /** + * Promotes the usage of node:assert/strict over node:assert. + */ + useNodeAssertStrict?: RuleFixConfiguration | null; + /** + * Enforces using the node: protocol for Node.js builtin modules. + */ + useNodejsImportProtocol?: RuleFixConfiguration | null; + /** + * Use the Number properties instead of global ones. + */ + useNumberNamespace?: RuleFixConfiguration | null; + /** + * Disallow parseInt() and Number.parseInt() in favor of binary, octal, and hexadecimal literals + */ + useNumericLiterals?: RuleFixConfiguration | null; + /** + * Prevent extra closing tags for components without children + */ + useSelfClosingElements?: RuleFixConfiguration | null; + /** + * When expressing array types, this rule promotes the usage of T\[] shorthand instead of Array\. + */ + useShorthandArrayType?: RuleFixConfiguration | null; + /** + * Require assignment operator shorthand where possible. + */ + useShorthandAssign?: RuleFixConfiguration | null; + /** + * Enforce using function types instead of object type with call signatures. + */ + useShorthandFunctionType?: RuleFixConfiguration | null; + /** + * Enforces switch clauses have a single statement, emits a quick fix wrapping the statements in a block. + */ + useSingleCaseStatement?: RuleFixConfiguration | null; + /** + * Disallow multiple variable declarations in the same variable statement + */ + useSingleVarDeclarator?: RuleFixConfiguration | null; + /** + * Prefer template literals over string concatenation. + */ + useTemplate?: RuleFixConfiguration | null; + /** + * Enforce the use of while loops instead of for loops when the initializer and update expressions are not needed. + */ + useWhile?: RuleFixConfiguration | null; +} +export interface RuleWithRestrictedGlobalsOptions { + /** + * The severity of the emitted diagnostics by the rule + */ + level: RulePlainConfiguration; + /** + * Rule's options + */ + options: RestrictedGlobalsOptions; +} +/** + * Options for the rule `noRestrictedGlobals`. + */ +export interface RestrictedGlobalsOptions { + /** + * A list of names that should trigger the rule + */ + deniedGlobals: string[]; +} +export interface RuleWithConsistentArrayTypeOptions { + /** + * The kind of the code actions emitted by the rule + */ + fix?: FixKind | null; + /** + * The severity of the emitted diagnostics by the rule + */ + level: RulePlainConfiguration; + /** + * Rule's options + */ + options: ConsistentArrayTypeOptions; +} +export interface ConsistentArrayTypeOptions { + syntax: ConsistentArrayType; +} +export interface RuleWithFilenamingConventionOptions { + /** + * The severity of the emitted diagnostics by the rule + */ + level: RulePlainConfiguration; + /** + * Rule's options + */ + options: FilenamingConventionOptions; +} +/** + * Rule's options. + */ +export interface FilenamingConventionOptions { + /** + * Allowed cases for file names. + */ + filenameCases?: FilenameCases; + /** + * If `false`, then non-ASCII characters are allowed. + */ + requireAscii?: boolean; + /** + * If `false`, then consecutive uppercase are allowed in _camel_ and _pascal_ cases. This does not affect other [Case]. + */ + strictCase?: boolean; +} +export interface RuleWithNamingConventionOptions { + /** + * The kind of the code actions emitted by the rule + */ + fix?: FixKind | null; + /** + * The severity of the emitted diagnostics by the rule + */ + level: RulePlainConfiguration; + /** + * Rule's options + */ + options: NamingConventionOptions; +} +/** + * Rule's options. + */ +export interface NamingConventionOptions { + /** + * Custom conventions. + */ + conventions?: Convention[]; + /** + * Allowed cases for _TypeScript_ `enum` member names. + */ + enumMemberCase?: Format; + /** + * If `false`, then non-ASCII characters are allowed. + */ + requireAscii?: boolean; + /** + * If `false`, then consecutive uppercase are allowed in _camel_ and _pascal_ cases. This does not affect other [Case]. + */ + strictCase?: boolean; +} +export interface Convention { + /** + * String cases to enforce + */ + formats?: Formats; + /** + * Regular expression to enforce + */ + match?: Regex | null; + /** + * Declarations concerned by this convention + */ + selector?: Selector; +} +export interface Selector { + /** + * Declaration kind + */ + kind?: Kind; + /** + * Modifiers used on the declaration + */ + modifiers?: Modifiers; + /** + * Scope of the declaration + */ + scope?: Scope; +} +/** + * A list of rules that belong to this group + */ +export interface Suspicious { + /** + * It enables ALL rules for this group. + */ + all?: boolean | null; + /** + * Use standard constants instead of approximated literals. + */ + noApproximativeNumericConstant?: RuleFixConfiguration | null; + /** + * Discourage the usage of Array index in keys. + */ + noArrayIndexKey?: RuleConfiguration | null; + /** + * Disallow assignments in expressions. + */ + noAssignInExpressions?: RuleConfiguration | null; + /** + * Disallows using an async function as a Promise executor. + */ + noAsyncPromiseExecutor?: RuleConfiguration | null; + /** + * Disallow reassigning exceptions in catch clauses. + */ + noCatchAssign?: RuleConfiguration | null; + /** + * Disallow reassigning class members. + */ + noClassAssign?: RuleConfiguration | null; + /** + * Prevent comments from being inserted as text nodes + */ + noCommentText?: RuleFixConfiguration | null; + /** + * Disallow comparing against -0 + */ + noCompareNegZero?: RuleFixConfiguration | null; + /** + * Disallow labeled statements that are not loops. + */ + noConfusingLabels?: RuleConfiguration | null; + /** + * Disallow void type outside of generic or return types. + */ + noConfusingVoidType?: RuleFixConfiguration | null; + /** + * Disallow the use of console.log + */ + noConsoleLog?: RuleFixConfiguration | null; + /** + * Disallow TypeScript const enum + */ + noConstEnum?: RuleFixConfiguration | null; + /** + * Prevents from having control characters and some escape sequences that match control characters in regular expressions. + */ + noControlCharactersInRegex?: RuleConfiguration | null; + /** + * Disallow the use of debugger + */ + noDebugger?: RuleFixConfiguration | null; + /** + * Require the use of === and !== + */ + noDoubleEquals?: RuleFixConfiguration | null; + /** + * Disallow duplicate case labels. + */ + noDuplicateCase?: RuleConfiguration | null; + /** + * Disallow duplicate class members. + */ + noDuplicateClassMembers?: RuleConfiguration | null; + /** + * Prevents JSX properties to be assigned multiple times. + */ + noDuplicateJsxProps?: RuleConfiguration | null; + /** + * Prevents object literals having more than one property declaration for the same name. + */ + noDuplicateObjectKeys?: RuleFixConfiguration | null; + /** + * Disallow duplicate function parameter name. + */ + noDuplicateParameters?: RuleConfiguration | null; + /** + * A describe block should not contain duplicate hooks. + */ + noDuplicateTestHooks?: RuleConfiguration | null; + /** + * Disallow empty block statements and static blocks. + */ + noEmptyBlockStatements?: RuleConfiguration | null; + /** + * Disallow the declaration of empty interfaces. + */ + noEmptyInterface?: RuleFixConfiguration | null; + /** + * Disallow the any type usage. + */ + noExplicitAny?: RuleConfiguration | null; + /** + * Disallow using export or module.exports in files containing tests + */ + noExportsInTest?: RuleConfiguration | null; + /** + * Prevents the wrong usage of the non-null assertion operator (!) in TypeScript files. + */ + noExtraNonNullAssertion?: RuleFixConfiguration | null; + /** + * Disallow fallthrough of switch clauses. + */ + noFallthroughSwitchClause?: RuleConfiguration | null; + /** + * Disallow focused tests. + */ + noFocusedTests?: RuleFixConfiguration | null; + /** + * Disallow reassigning function declarations. + */ + noFunctionAssign?: RuleConfiguration | null; + /** + * Disallow assignments to native objects and read-only global variables. + */ + noGlobalAssign?: RuleConfiguration | null; + /** + * Use Number.isFinite instead of global isFinite. + */ + noGlobalIsFinite?: RuleFixConfiguration | null; + /** + * Use Number.isNaN instead of global isNaN. + */ + noGlobalIsNan?: RuleFixConfiguration | null; + /** + * Disallow use of implicit any type on variable declarations. + */ + noImplicitAnyLet?: RuleConfiguration | null; + /** + * Disallow assigning to imported bindings + */ + noImportAssign?: RuleConfiguration | null; + /** + * Disallow labels that share a name with a variable + */ + noLabelVar?: RuleConfiguration | null; + /** + * Disallow characters made with multiple code points in character class syntax. + */ + noMisleadingCharacterClass?: RuleFixConfiguration | null; + /** + * Enforce proper usage of new and constructor. + */ + noMisleadingInstantiator?: RuleConfiguration | null; + /** + * Disallow shorthand assign when variable appears on both sides. + */ + noMisrefactoredShorthandAssign?: RuleFixConfiguration | null; + /** + * Disallow direct use of Object.prototype builtins. + */ + noPrototypeBuiltins?: RuleConfiguration | null; + /** + * Disallow variable, function, class, and type redeclarations in the same scope. + */ + noRedeclare?: RuleConfiguration | null; + /** + * Prevents from having redundant "use strict". + */ + noRedundantUseStrict?: RuleFixConfiguration | null; + /** + * Disallow comparisons where both sides are exactly the same. + */ + noSelfCompare?: RuleConfiguration | null; + /** + * Disallow identifiers from shadowing restricted names. + */ + noShadowRestrictedNames?: RuleConfiguration | null; + /** + * Disallow disabled tests. + */ + noSkippedTests?: RuleFixConfiguration | null; + /** + * Disallow sparse arrays + */ + noSparseArray?: RuleFixConfiguration | null; + /** + * It detects possible "wrong" semicolons inside JSX elements. + */ + noSuspiciousSemicolonInJsx?: RuleConfiguration | null; + /** + * Disallow then property. + */ + noThenProperty?: RuleConfiguration | null; + /** + * Disallow unsafe declaration merging between interfaces and classes. + */ + noUnsafeDeclarationMerging?: RuleConfiguration | null; + /** + * Disallow using unsafe negation. + */ + noUnsafeNegation?: RuleFixConfiguration | null; + /** + * It enables the recommended rules for this group + */ + recommended?: boolean | null; + /** + * Ensure async functions utilize await. + */ + useAwait?: RuleConfiguration | null; + /** + * Enforce default clauses in switch statements to be last + */ + useDefaultSwitchClauseLast?: RuleConfiguration | null; + /** + * Enforce get methods to always return a value. + */ + useGetterReturn?: RuleConfiguration | null; + /** + * Use Array.isArray() instead of instanceof Array. + */ + useIsArray?: RuleFixConfiguration | null; + /** + * Require using the namespace keyword over the module keyword to declare TypeScript namespaces. + */ + useNamespaceKeyword?: RuleFixConfiguration | null; + /** + * This rule verifies the result of typeof $expr unary expressions is being compared to valid values, either string literals containing valid type names or other typeof expressions + */ + useValidTypeof?: RuleFixConfiguration | null; +} +export interface OrganizeImports { + /** + * Enables the organization of imports + */ + enabled?: boolean | null; + /** + * A list of Unix shell style patterns. The formatter will ignore files/folders that will match these patterns. + */ + ignore?: StringSet | null; + /** + * A list of Unix shell style patterns. The formatter will include files/folders that will match these patterns. + */ + include?: StringSet | null; +} +export interface OverridePattern { + /** + * Specific configuration for the Css language + */ + css?: CssConfiguration | null; + /** + * Specific configuration for the Json language + */ + formatter?: OverrideFormatterConfiguration | null; + /** + * A list of Unix shell style patterns. The formatter will ignore files/folders that will match these patterns. + */ + ignore?: StringSet | null; + /** + * A list of Unix shell style patterns. The formatter will include files/folders that will match these patterns. + */ + include?: StringSet | null; + /** + * Specific configuration for the JavaScript language + */ + javascript?: JavascriptConfiguration | null; + /** + * Specific configuration for the Json language + */ + json?: JsonConfiguration | null; + /** + * Specific configuration for the Json language + */ + linter?: OverrideLinterConfiguration | null; + /** + * Specific configuration for the Json language + */ + organizeImports?: OverrideOrganizeImportsConfiguration | null; +} +export interface OverrideFormatterConfiguration { + /** + * The attribute position style. + */ + attributePosition?: AttributePosition | null; + enabled?: boolean | null; + /** + * Stores whether formatting should be allowed to proceed if a given file has syntax errors + */ + formatWithErrors?: boolean | null; + /** + * The size of the indentation, 2 by default (deprecated, use `indent-width`) + */ + indentSize?: IndentWidth | null; + /** + * The indent style. + */ + indentStyle?: PlainIndentStyle | null; + /** + * The size of the indentation, 2 by default + */ + indentWidth?: IndentWidth | null; + /** + * The type of line ending. + */ + lineEnding?: LineEnding | null; + /** + * What's the max width of a line. Defaults to 80. + */ + lineWidth?: LineWidth | null; +} +export interface OverrideLinterConfiguration { + /** + * if `false`, it disables the feature and the linter won't be executed. `true` by default + */ + enabled?: boolean | null; + /** + * List of rules + */ + rules?: Rules | null; +} +export interface OverrideOrganizeImportsConfiguration { + /** + * if `false`, it disables the feature and the linter won't be executed. `true` by default + */ + enabled?: boolean | null; +} +/** + * Set of properties to integrate Biome with a VCS software. + */ +export interface VcsConfiguration { + /** + * The kind of client. + */ + clientKind?: VcsClientKind | null; + /** + * The main branch of the project + */ + defaultBranch?: string | null; + /** + * Whether Biome should integrate itself with the VCS client + */ + enabled?: boolean | null; + /** + * The folder where Biome should check for VCS files. By default, Biome will use the same folder where `biome.json` was found. + * + * If Biome can't find the configuration, it will attempt to use the current working directory. If no current working directory can't be found, Biome won't use the VCS integration, and a diagnostic will be emitted + */ + root?: string | null; + /** + * Whether Biome should use the VCS ignore file. When [true], Biome will ignore the files specified in the ignore file. + */ + useIgnoreFile?: boolean | null; +} diff --git a/build-tools/packages/build-tools/src/common/fluidRepo.ts b/build-tools/packages/build-tools/src/common/fluidRepo.ts index e3ced816100e..d4dbbbd6efd8 100644 --- a/build-tools/packages/build-tools/src/common/fluidRepo.ts +++ b/build-tools/packages/build-tools/src/common/fluidRepo.ts @@ -5,318 +5,47 @@ import * as path from "path"; -import { - DEFAULT_INTERDEPENDENCY_RANGE, - InterdependencyRange, - VersionBumpType, -} from "@fluid-tools/version-tools"; - -import registerDebug from "debug"; import { TaskDefinitionsOnDisk } from "./fluidTaskDefinitions"; -import { loadFluidBuildConfig } from "./fluidUtils"; import { MonoRepo } from "./monoRepo"; import { Package, Packages } from "./npmPackage"; import { ExecAsyncResult } from "./utils"; -const traceInit = registerDebug("fluid-build:init"); /** - * Fluid build configuration that is expected in the repo-root package.json. - */ -export interface IFluidBuildConfig { - /** - * Build tasks and dependencies definitions - */ - tasks?: TaskDefinitionsOnDisk; - - /** - * A mapping of package or release group names to metadata about the package or release group. This can only be - * configured in the repo-wide Fluid build config (the repo-root package.json). - */ - repoPackages?: { - [name: string]: IFluidRepoPackageEntry; - }; - - /** - * Policy configuration for the `check:policy` command. This can only be configured in the repo-wide Fluid build - * config (the repo-root package.json). - */ - policy?: PolicyConfig; - - /** - * Configuration for assert tagging. - */ - assertTagging?: AssertTaggingConfig; - - /** - * A mapping of branch names to previous version baseline styles. The type test generator takes this information - * into account when calculating the baseline version to use when it's run on a particular branch. If this is not - * defined for a branch or package, then that package will be skipped during type test generation. - */ - branchReleaseTypes?: { - [name: string]: VersionBumpType | PreviousVersionStyle; - }; - - /** - * Configuration for the `generate:releaseNotes` command. - */ - releaseNotes?: ReleaseNotesConfig; -} - -/** - * A type representing the different version constraint styles we use when determining the previous version for type - * test generation. - * - * The "base" versions are calculated by zeroing out all version segments lower than the base. That is, for a version v, - * the baseMajor version is `${v.major}.0.0` and the baseMinor version is `${v.major}.${v.minor}.0`. - * - * The "previous" versions work similarly, but the major/minor/patch segment is reduced by 1. That is, for a version v, - * the previousMajor version is `${min(v.major - 1, 1)}.0.0`, the previousMinor version is - * `${v.major}.${min(v.minor - 1, 0)}.0`, and the previousPatch is `${v.major}.${v.minor}.${min(v.patch - 1, 0)}.0`. - * - * The "previous" versions never roll back below 1 for the major version and 0 for minor and patch. That is, the - * previousMajor, previousMinor, and previousPatch versions for `1.0.0` are all `1.0.0`. - * - * @example + * The version of the fluidBuild configuration currently used. * - * Given the version 2.3.5: + * @remarks * - * baseMajor: 2.0.0 - * baseMinor: 2.3.0 - * ~baseMinor: ~2.3.0 - * previousPatch: 2.3.4 - * previousMinor: 2.2.0 - * previousMajor: 1.0.0 - * ^previousMajor: ^1.0.0 - * ^previousMinor: ^2.2.0 - * ~previousMajor: ~1.0.0 - * ~previousMinor: ~2.2.0 - * - * @example - * - * Given the version 2.0.0-internal.2.3.5: - * - * baseMajor: 2.0.0-internal.2.0.0 - * baseMinor: 2.0.0-internal.2.3.0 - * ~baseMinor: >=2.0.0-internal.2.3.0 <2.0.0-internal.3.0.0 - * previousPatch: 2.0.0-internal.2.3.4 - * previousMinor: 2.0.0-internal.2.2.0 - * previousMajor: 2.0.0-internal.1.0.0 - * ^previousMajor: >=2.0.0-internal.1.0.0 <2.0.0-internal.2.0.0 - * ^previousMinor: >=2.0.0-internal.2.2.0 <2.0.0-internal.3.0.0 - * ~previousMajor: >=2.0.0-internal.1.0.0 <2.0.0-internal.1.1.0 - * ~previousMinor: >=2.0.0-internal.2.2.0 <2.0.0-internal.2.2.0 - * - * @example - * - * Given the version 2.0.0-internal.2.0.0: - * - * baseMajor: 2.0.0-internal.2.0.0 - * baseMinor: 2.0.0-internal.2.0.0 - * ~baseMinor: >=2.0.0-internal.2.0.0 <2.0.0-internal.2.1.0 - * previousPatch: 2.0.0-internal.2.0.0 - * previousMinor: 2.0.0-internal.2.0.0 - * previousMajor: 2.0.0-internal.1.0.0 - * ^previousMajor: >=2.0.0-internal.1.0.0 <2.0.0-internal.2.0.0 - * ^previousMinor: >=2.0.0-internal.2.0.0 <2.0.0-internal.3.0.0 - * ~previousMajor: >=2.0.0-internal.1.0.0 <2.0.0-internal.1.1.0 - * ~previousMinor: >=2.0.0-internal.2.0.0 <2.0.0-internal.2.1.0 - * - * @internal - */ -export type PreviousVersionStyle = - | "baseMajor" - | "baseMinor" - | "previousPatch" - | "previousMinor" - | "previousMajor" - | "~baseMinor" - | "^previousMajor" - | "^previousMinor" - | "~previousMajor" - | "~previousMinor"; - -/** - * A short name for the section. Each section in a {@link ReleaseNotesConfig} must have a unique name. - */ -export type ReleaseNotesSectionName = string; - -/** - * Configuration for a release notes section. - */ -export interface ReleaseNotesSection { - /** - * A full string to serve as the heading for the section when displayed in release notes. - */ - heading: string; -} - -/** - * Configuration for the `generate:releaseNotes` command. If this configuration is not present in the config, the - * `generate:releaseNotes` command will report an error. + * This is not exported outside of the build-tools package; it is only used internally. */ -export interface ReleaseNotesConfig { - sections: Record; -} +export const FLUIDBUILD_CONFIG_VERSION = 1; /** - * Policy configuration for the `check:policy` command. + * Top-most configuration for repo build settings. */ -export interface PolicyConfig { - additionalLockfilePaths?: string[]; - pnpmSinglePackageWorkspace?: string[]; - fluidBuildTasks: { - tsc: { - ignoreTasks: string[]; - ignoreDependencies: string[]; - ignoreDevDependencies: string[]; - }; - }; - dependencies?: { - commandPackages: [string, string][]; - }; - /** - * An array of strings/regular expressions. Paths that match any of these expressions will be completely excluded from - * policy-check. - */ - exclusions?: string[]; - - /** - * An object with handler name as keys that maps to an array of strings/regular expressions to - * exclude that rule from being checked. - */ - handlerExclusions?: { [rule: string]: string[] }; - - packageNames?: PackageNamePolicyConfig; - - /** - * (optional) requirements to enforce against each public package. - */ - publicPackageRequirements?: PackageRequirements; -} - -export interface AssertTaggingConfig { - assertionFunctions: { [functionName: string]: number }; - - /** - * An array of paths under which assert tagging applies to. If this setting is provided, only packages whose paths - * match the regular expressions in this setting will be assert-tagged. - */ - enabledPaths?: RegExp[]; -} - -/** - * Configuration for package naming and publication policies. - */ -export interface PackageNamePolicyConfig { - /** - * A list of package scopes that are permitted in the repo. - */ - allowedScopes?: string[]; - /** - * A list of packages that have no scope. - */ - unscopedPackages?: string[]; - /** - * Packages that must be published. - */ - mustPublish: { - /** - * A list of package names or scopes that must publish to npm, and thus should never be marked private. - */ - npm?: string[]; - - /** - * A list of package names or scopes that must publish to an internal feed, and thus should always be marked - * private. - */ - internalFeed?: string[]; - }; - - /** - * Packages that may or may not be published. - */ - mayPublish: { - /** - * A list of package names or scopes that may publish to npm, and thus might or might not be marked private. - */ - npm?: string[]; - - /** - * A list of package names or scopes that must publish to an internal feed, and thus might or might not be marked - * private. - */ - internalFeed?: string[]; - }; -} - -/** - * Expresses requirements for a given package, applied to its package.json. - */ -export interface PackageRequirements { - /** - * (optional) list of script requirements for the package. - */ - requiredScripts?: ScriptRequirement[]; - - /** - * (optional) list of required dev dependencies for the package. - * @remarks Note: there is no enforcement of version requirements, only that a dependency on the specified name must exist. - */ - requiredDevDependencies?: string[]; -} - -/** - * Requirements for a given script. - */ -export interface ScriptRequirement { - /** - * Name of the script to check. - */ - name: string; - - /** - * Body of the script being checked. - * A contents match will be enforced iff {@link ScriptRequirement.bodyMustMatch}. - * This value will be used as the default contents inserted by the policy resolver (regardless of {@link ScriptRequirement.bodyMustMatch}). - */ - body: string; - +export interface IFluidBuildConfig { /** - * Whether or not the script body is required to match {@link ScriptRequirement.body} when running the policy checker. - * @defaultValue `false` + * The version of the config. + * + * IMPORTANT: this will become required in a future release. */ - bodyMustMatch?: boolean; -} - -/** - * Metadata about known-broken types. - */ -export interface BrokenCompatSettings { - backCompat?: false; - forwardCompat?: false; -} - -/** - * A mapping of a type name to its {@link BrokenCompatSettings}. - */ -export type BrokenCompatTypes = Partial>; + version?: typeof FLUIDBUILD_CONFIG_VERSION; -export interface ITypeValidationConfig { /** - * An object containing types that are known to be broken. + * Build tasks and dependencies definitions */ - broken: BrokenCompatTypes; + tasks?: TaskDefinitionsOnDisk; /** - * If true, disables type test preparation and generation for the package. + * A mapping of package or release group names to metadata about the package or release group. This can only be + * configured in the repo-wide Fluid build config (the repo-root package.json). */ - disabled?: boolean; + repoPackages?: IFluidBuildDirs; } /** * Configures a package or release group */ -export interface IFluidRepoPackage { +export interface IFluidBuildDir { /** * The path to the package. For release groups this should be the path to the root of the release group. */ @@ -326,19 +55,13 @@ export interface IFluidRepoPackage { * An array of paths under `directory` that should be ignored. */ ignoredDirs?: string[]; - - /** - * The interdependencyRange controls the type of semver range to use between packages in the same release group. This - * setting controls the default range that will be used when updating the version of a release group. The default can - * be overridden using the `--interdependencyRange` flag in the `flub bump` command. - */ - defaultInterdependencyRange: InterdependencyRange; } -export type IFluidRepoPackageEntry = - | string - | IFluidRepoPackage - | (string | IFluidRepoPackage)[]; +export type IFluidBuildDirEntry = string | IFluidBuildDir | (string | IFluidBuildDir)[]; + +export interface IFluidBuildDirs { + [name: string]: IFluidBuildDirEntry; +} export class FluidRepo { private readonly _releaseGroups = new Map(); @@ -349,46 +72,34 @@ export class FluidRepo { public readonly packages: Packages; - public static create(resolvedRoot: string) { - const packageManifest = loadFluidBuildConfig(resolvedRoot); - return new FluidRepo(resolvedRoot, packageManifest); - } - - protected constructor( + public constructor( public readonly resolvedRoot: string, - packageManifest: IFluidBuildConfig, + fluidBuildDirs?: IFluidBuildDirs, ) { // Expand to full IFluidRepoPackage and full path - const normalizeEntry = ( - item: IFluidRepoPackageEntry, - ): IFluidRepoPackage | IFluidRepoPackage[] => { + const normalizeEntry = (item: IFluidBuildDirEntry): IFluidBuildDir | IFluidBuildDir[] => { if (Array.isArray(item)) { - return item.map((entry) => normalizeEntry(entry) as IFluidRepoPackage); + return item.map((entry) => normalizeEntry(entry) as IFluidBuildDir); } if (typeof item === "string") { - traceInit( - `No defaultInterdependencyRange setting found for '${item}'. Defaulting to "${DEFAULT_INTERDEPENDENCY_RANGE}".`, - ); return { directory: path.join(resolvedRoot, item), ignoredDirs: undefined, - defaultInterdependencyRange: DEFAULT_INTERDEPENDENCY_RANGE, }; } const directory = path.join(resolvedRoot, item.directory); return { directory, ignoredDirs: item.ignoredDirs?.map((dir) => path.join(directory, dir)), - defaultInterdependencyRange: item.defaultInterdependencyRange, }; }; - const loadOneEntry = (item: IFluidRepoPackage, group: string) => { + const loadOneEntry = (item: IFluidBuildDir, group: string) => { return Packages.loadDir(item.directory, group, item.ignoredDirs); }; const loadedPackages: Package[] = []; - for (const group in packageManifest.repoPackages) { - const item = normalizeEntry(packageManifest.repoPackages[group]); + for (const group in fluidBuildDirs) { + const item = normalizeEntry(fluidBuildDirs[group]); if (Array.isArray(item)) { for (const i of item) { loadedPackages.push(...loadOneEntry(i, group)); diff --git a/build-tools/packages/build-tools/src/common/fluidUtils.ts b/build-tools/packages/build-tools/src/common/fluidUtils.ts index 7c31ed2dcb89..d2c2d20f569a 100644 --- a/build-tools/packages/build-tools/src/common/fluidUtils.ts +++ b/build-tools/packages/build-tools/src/common/fluidUtils.ts @@ -6,18 +6,19 @@ import * as childProcess from "node:child_process"; import { existsSync } from "node:fs"; import * as path from "node:path"; -import { cosmiconfigSync } from "cosmiconfig"; - import { getPackages } from "@manypkg/get-packages"; +import { cosmiconfigSync } from "cosmiconfig"; +import registerDebug from "debug"; import { readJson } from "fs-extra"; + import { commonOptions } from "./commonOptions"; -import { IFluidBuildConfig } from "./fluidRepo"; +import { FLUIDBUILD_CONFIG_VERSION, IFluidBuildConfig } from "./fluidRepo"; +import { defaultLogger } from "./logging"; import { realpathAsync } from "./utils"; // switch to regular import once building ESM const findUp = import("find-up"); -import registerDebug from "debug"; const traceInit = registerDebug("fluid-build:init"); async function isFluidRootPackage(dir: string) { @@ -127,32 +128,18 @@ export async function getResolvedFluidRoot(buildRoot = false) { return await realpathAsync(resolvedRoot); } +const configName = "fluidBuild"; + /** - * A cosmiconfig explorer to find the fluidBuild config. First looks for javascript config files and falls back to the - * fluidBuild property in package.json. We create a single explorer here because cosmiconfig internally caches configs + * A cosmiconfig explorer to find the fluidBuild config. First looks for JavaScript config files and falls back to the + * `fluidBuild` property in package.json. We create a single explorer here because cosmiconfig internally caches configs * for performance. The cache is per-explorer, so re-using the same explorer is a minor perf improvement. */ -const configExplorer = cosmiconfigSync("fluidBuild", { - searchPlaces: [`fluidBuild.config.cjs`, `fluidBuild.config.js`, "package.json"], - packageProp: "fluidBuild", +const configExplorer = cosmiconfigSync(configName, { + searchPlaces: [`${configName}.config.cjs`, `${configName}.config.js`, "package.json"], + packageProp: [configName], }); -/** - * Loads an IFluidBuildConfig from the fluidBuild property in a package.json file, or from fluidBuild.config.[c]js. - * Throw if not found. - * - * @param rootDir - The path to the root package.json to load. - * @param noCache - If true, the config cache will be cleared and the config will be reloaded. - * @returns The fluidBuild section of the package.json. - */ -export function loadFluidBuildConfig(rootDir: string, noCache = false): IFluidBuildConfig { - const config = getFluidBuildConfig(rootDir, noCache); - if (config === undefined) { - throw new Error(`Error loading config.`); - } - return config; -} - /** * Get an IFluidBuildConfig from the fluidBuild property in a package.json file, or from fluidBuild.config.[c]js. * @@ -160,11 +147,34 @@ export function loadFluidBuildConfig(rootDir: string, noCache = false): IFluidBu * @param noCache - If true, the config cache will be cleared and the config will be reloaded. * @returns The fluidBuild section of the package.json, or undefined if not found */ -export function getFluidBuildConfig(rootDir: string, noCache = false): IFluidBuildConfig { +export function getFluidBuildConfig( + rootDir: string, + noCache = false, + log = defaultLogger, +): IFluidBuildConfig { if (noCache === true) { configExplorer.clearCaches(); } - const config = configExplorer.search(rootDir); - return config?.config; + const configResult = configExplorer.search(rootDir); + const config = configResult?.config as IFluidBuildConfig | undefined; + + if (config === undefined) { + throw new Error("No fluidBuild configuration found."); + } + + if (config.version === undefined) { + log.warning( + "fluidBuild config has no version field. This field will be required in a future release.", + ); + config.version = FLUIDBUILD_CONFIG_VERSION; + } + + // Only version 1 of the config is supported. If any other value is provided, throw an error. + if (config.version !== FLUIDBUILD_CONFIG_VERSION) { + throw new Error( + `Configuration version is not supported: ${config?.version}. Config version must be ${FLUIDBUILD_CONFIG_VERSION}.`, + ); + } + return config; } diff --git a/build-tools/packages/build-tools/src/common/gitRepo.ts b/build-tools/packages/build-tools/src/common/gitRepo.ts index e749076947b1..aa716797df77 100644 --- a/build-tools/packages/build-tools/src/common/gitRepo.ts +++ b/build-tools/packages/build-tools/src/common/gitRepo.ts @@ -3,10 +3,8 @@ * Licensed under the MIT License. */ -import path from "node:path"; import { parseISO } from "date-fns"; import registerDebug from "debug"; -import { statSync } from "fs-extra"; import { exec, execNoError } from "./utils"; const traceGitRepo = registerDebug("fluid-build:gitRepo"); @@ -217,18 +215,58 @@ export class GitRepo { } /** - * Returns an array containing all the files in the the provided path. - * Returned paths are rooted at the root of the repo. + * Returns an array containing repo root-relative paths to files that are deleted in the working tree. */ - public async getFiles(directory: string): Promise { - const results = await this.exec( - `ls-files -co --exclude-standard --full-name -- ${directory}`, - `get files`, - ); + public async getDeletedFiles(): Promise { + const results = await this.exec(`status --porcelain`, `get deleted files`); return results .split("\n") - .map((line) => line.trim()) - .filter((file) => statSync(path.resolve(this.resolvedRoot, file)).isFile()); + .filter((t) => t.startsWith(" D ")) + .map((t) => t.substring(3)); + } + + /** + * Returns an array containing repo repo-relative paths to all the files in the provided directory. + * A given path will only be included once in the array; that is, there will be no duplicate paths. + * Note that this function excludes files that are deleted locally whether the deletion is staged or not. + * + * @param directory - A directory to filter the results by. Only files under this directory will be returned. To + * return all files in the repo use the value `"."`. + */ + public async getFiles(directory: string): Promise { + /** + * What these git ls-files flags do: + * + * ``` + * --cached: Includes cached (staged) files. + * --others: Includes other (untracked) files that are not ignored. + * --exclude-standard: Excludes files that are ignored by standard ignore rules. + * --deduplicate: Removes duplicate entries from the output. + * --full-name: Shows the full path of the files relative to the repository root. + * ``` + */ + const command = `ls-files --cached --others --exclude-standard --deduplicate --full-name -- ${directory}`; + const [fileResults, deletedFiles] = await Promise.all([ + this.exec(command, `get files`), + this.getDeletedFiles(), + ]); + + // This includes paths to deleted, unstaged files, so we get the list of deleted files from git status and remove + // those from the full list. + const allFiles = new Set( + fileResults + .split("\n") + .map((line) => line.trim()) + // filter out empty lines + .filter((line) => line !== ""), + ); + + for (const deletedFile of deletedFiles) { + allFiles.delete(deletedFile); + } + + // Files are already repo root-relative + return [...allFiles]; } /** diff --git a/build-tools/packages/build-tools/src/common/monoRepo.ts b/build-tools/packages/build-tools/src/common/monoRepo.ts index 9037241a69d4..c967d08142cc 100644 --- a/build-tools/packages/build-tools/src/common/monoRepo.ts +++ b/build-tools/packages/build-tools/src/common/monoRepo.ts @@ -4,15 +4,11 @@ */ import * as path from "path"; -import { - DEFAULT_INTERDEPENDENCY_RANGE, - InterdependencyRange, -} from "@fluid-tools/version-tools"; import { getPackagesSync } from "@manypkg/get-packages"; import { readFileSync, readJsonSync } from "fs-extra"; import YAML from "yaml"; -import { IFluidBuildConfig, IFluidRepoPackage } from "./fluidRepo"; +import { IFluidBuildDir } from "./fluidRepo"; import { Logger, defaultLogger } from "./logging"; import { Package } from "./npmPackage"; import { execWithErrorAsync, existsSync, rimrafWithErrorAsync } from "./utils"; @@ -63,8 +59,8 @@ export class MonoRepo { return this.kind as "build-tools" | "client" | "server" | "gitrest" | "historian"; } - static load(group: string, repoPackage: IFluidRepoPackage) { - const { directory, ignoredDirs, defaultInterdependencyRange } = repoPackage; + static load(group: string, repoPackage: IFluidBuildDir) { + const { directory, ignoredDirs } = repoPackage; let packageManager: PackageManager; let packageDirs: string[]; @@ -93,24 +89,11 @@ export class MonoRepo { return undefined; } packageDirs = packages.filter((pkg) => pkg.relativeDir !== ".").map((pkg) => pkg.dir); - - if (defaultInterdependencyRange === undefined) { - traceInit( - `No defaultinterdependencyRange specified for ${group} release group. Defaulting to "${DEFAULT_INTERDEPENDENCY_RANGE}".`, - ); - } } catch { return undefined; } - return new MonoRepo( - group, - directory, - defaultInterdependencyRange ?? DEFAULT_INTERDEPENDENCY_RANGE, - packageManager, - packageDirs, - ignoredDirs, - ); + return new MonoRepo(group, directory, packageManager, packageDirs, ignoredDirs); } /** @@ -124,7 +107,6 @@ export class MonoRepo { constructor( public readonly kind: string, public readonly repoPath: string, - public readonly interdependencyRange: InterdependencyRange, private readonly packageManager: PackageManager, packageDirs: string[], ignoredDirs?: string[], @@ -201,10 +183,6 @@ export class MonoRepo { : "npm i --no-package-lock --no-shrinkwrap"; } - public get fluidBuildConfig(): IFluidBuildConfig | undefined { - return this.pkg.packageJson.fluidBuild; - } - public getNodeModulePath() { return path.join(this.repoPath, "node_modules"); } diff --git a/build-tools/packages/build-tools/src/common/npmPackage.ts b/build-tools/packages/build-tools/src/common/npmPackage.ts index c6572c39e9cc..041299e2fbe1 100644 --- a/build-tools/packages/build-tools/src/common/npmPackage.ts +++ b/build-tools/packages/build-tools/src/common/npmPackage.ts @@ -14,9 +14,10 @@ import sortPackageJson from "sort-package-json"; import type { SetRequired, PackageJson as StandardPackageJson } from "type-fest"; import { options } from "../fluidBuild/options"; -import { type IFluidBuildConfig, type ITypeValidationConfig } from "./fluidRepo"; +import { type IFluidBuildConfig } from "./fluidRepo"; import { defaultLogger } from "./logging"; import { MonoRepo, PackageManager } from "./monoRepo"; +import { type ITypeValidationConfig } from "./typeValidatorConfig"; import { ExecAsyncResult, execWithErrorAsync, diff --git a/build-tools/packages/build-tools/src/common/typeValidatorConfig.ts b/build-tools/packages/build-tools/src/common/typeValidatorConfig.ts new file mode 100644 index 000000000000..b751095dba09 --- /dev/null +++ b/build-tools/packages/build-tools/src/common/typeValidatorConfig.ts @@ -0,0 +1,29 @@ +/*! + * Copyright (c) Microsoft Corporation and contributors. All rights reserved. + * Licensed under the MIT License. + */ + +/** + * Metadata about known-broken types. + */ +export interface BrokenCompatSettings { + backCompat?: false; + forwardCompat?: false; +} + +/** + * A mapping of a type name to its {@link BrokenCompatSettings}. + */ +export type BrokenCompatTypes = Partial>; + +export interface ITypeValidationConfig { + /** + * An object containing types that are known to be broken. + */ + broken: BrokenCompatTypes; + + /** + * If true, disables type test preparation and generation for the package. + */ + disabled?: boolean; +} diff --git a/build-tools/packages/build-tools/src/fluidBuild/fluidRepoBuild.ts b/build-tools/packages/build-tools/src/fluidBuild/fluidRepoBuild.ts index 273eebc1f368..aaf412cac8f8 100644 --- a/build-tools/packages/build-tools/src/fluidBuild/fluidRepoBuild.ts +++ b/build-tools/packages/build-tools/src/fluidBuild/fluidRepoBuild.ts @@ -6,7 +6,7 @@ import * as path from "path"; import chalk from "chalk"; import registerDebug from "debug"; -import { FluidRepo, IFluidBuildConfig } from "../common/fluidRepo"; +import { FluidRepo, IFluidBuildDirs } from "../common/fluidRepo"; import { getFluidBuildConfig } from "../common/fluidUtils"; import { defaultLogger } from "../common/logging"; import { MonoRepo } from "../common/monoRepo"; @@ -40,10 +40,10 @@ export class FluidRepoBuild extends FluidRepo { root: "", }, }; - return new FluidRepoBuild(resolvedRoot, packageManifest); + return new FluidRepoBuild(resolvedRoot, packageManifest.repoPackages); } - private constructor(resolvedRoot: string, packageManifest: IFluidBuildConfig) { - super(resolvedRoot, packageManifest); + private constructor(resolvedRoot: string, repoPackages?: IFluidBuildDirs) { + super(resolvedRoot, repoPackages); } public async clean() { diff --git a/build-tools/packages/build-tools/src/fluidBuild/tasks/leaf/biomeTasks.ts b/build-tools/packages/build-tools/src/fluidBuild/tasks/leaf/biomeTasks.ts index 0445f4b4b238..0da001c2921e 100644 --- a/build-tools/packages/build-tools/src/fluidBuild/tasks/leaf/biomeTasks.ts +++ b/build-tools/packages/build-tools/src/fluidBuild/tasks/leaf/biomeTasks.ts @@ -3,26 +3,20 @@ * Licensed under the MIT License. */ -import path from "path"; +import { BiomeConfigReader } from "../../../common/biomeConfig"; import { getResolvedFluidRoot } from "../../../common/fluidUtils"; import { GitRepo } from "../../../common/gitRepo"; import { LeafWithFileStatDoneFileTask } from "./leafTask"; -// switch to regular import once building ESM -const findUp = import("find-up"); - /** - * This task enables incremental build support for Biome formatting tasks. It has important limitations. - * - * @remarks + * This task enables incremental build support for Biome formatting tasks. It reads Biome configuration files to load + * the 'include' and 'ignore' settings, and will not consider other files when checking if the task needs to run. * - * - The task does not read Biome configuration files to determine what files would be formatted. Instead it naively - * assumes all files would be formatted. - * - All Biome configuration files found when looking up from the package directory to the root of the repo are - * considered used, whether the file is used. + * The task will consider the 'extends' value and load nested Biome configs. The configs will be merged, but array-type + * settings like 'includes' and 'ignores' are not merged - the top-most config wins for such keys. * - * As of version 0.41.0, The task uses a content-based caching strategy, so it is less susceptible to invalidation than - * earlier versions which were based on file modification times. However, the limitations above still apply. + * Note that .gitignored paths are always excluded, regardless of the "vcs" setting in the Biome configuration. + * Internally the task uses git itself to enumerate files, and files that aren't enumerated are not considered. */ export class BiomeTask extends LeafWithFileStatDoneFileTask { // performance note: having individual tasks each acquire repo root and GitRepo @@ -30,6 +24,9 @@ export class BiomeTask extends LeafWithFileStatDoneFileTask { // to task constructors. private readonly repoRoot = getResolvedFluidRoot(true); private readonly gitRepo = this.repoRoot.then((repoRoot) => new GitRepo(repoRoot)); + private readonly biomeConfig = this.gitRepo.then((gitRepo) => + BiomeConfigReader.create(this.node.pkg.directory, gitRepo), + ); /** * Use hashes instead of modified times in donefile. @@ -39,41 +36,17 @@ export class BiomeTask extends LeafWithFileStatDoneFileTask { } /** - * Includes all files in the task's package directory and any biome config files in the directory tree. Files ignored - * by git are excluded. + * Includes all files in the the task's package directory that Biome would format and any Biome config files that + * apply to the directory. Files ignored by git are excluded. */ protected async getInputFiles(): Promise { - const repoRoot = await this.repoRoot; - const gitRepo = await this.gitRepo; - - const configFiles = await this.getBiomeConfigPaths(this.node.pkg.directory); - const files = (await gitRepo.getFiles(this.node.pkg.directory)).map((file) => - path.join(repoRoot, file), - ); - - return [...new Set([...configFiles, ...files])]; + const biomeConfig = await this.biomeConfig; + // Absolute paths to files that would be formatted by biome. + const { formattedFiles, allConfigs } = biomeConfig; + return [...new Set([...allConfigs, ...formattedFiles])]; } protected async getOutputFiles(): Promise { - // Input and output files are the same. - return this.getInputFiles(); - } - - /** - * Returns an array of all the biome config files found from the current working directory up to the root of the repo. - * - * Rather than parse and read the config files, this implementation naively searches for all config files from the - * task's package directory up to the root of the repo and assumes they're all used. In the future we might want to - * parse the config files anyway to extract ignore paths, at which point this implementation can change. - */ - private async getBiomeConfigPaths(cwd: string): Promise { - return (await findUp) - .findUpMultiple(["biome.json", "biome.jsonc"], { cwd, stopAt: await this.repoRoot }) - .then((configs) => { - if (configs.length === 0) { - this.traceError(`Can't find biome config file`); - } - return configs; - }); + return (await this.biomeConfig).formattedFiles; } } diff --git a/build-tools/packages/build-tools/src/fluidBuild/tasks/leaf/leafTask.ts b/build-tools/packages/build-tools/src/fluidBuild/tasks/leaf/leafTask.ts index d8ec718a54c7..80c5e230786d 100644 --- a/build-tools/packages/build-tools/src/fluidBuild/tasks/leaf/leafTask.ts +++ b/build-tools/packages/build-tools/src/fluidBuild/tasks/leaf/leafTask.ts @@ -498,8 +498,11 @@ export abstract class LeafWithDoneFileTask extends LeafTask { return true; } this.traceTrigger(`mismatched compare file: ${doneFileFullPath}`); - traceTaskTrigger(doneFileExpectedContent); - traceTaskTrigger(doneFileContent); + // These log statements can be useful for debugging, but they're extremely long and completely + // obscure other logs. + // In the future we can consider logging just the diff between the input and output. + // this.traceTrigger(doneFileExpectedContent); + // this.traceTrigger(doneFileContent); } else { this.traceTrigger( "unable to generate done file expected content (getDoneFileContent returned undefined)", @@ -555,11 +558,12 @@ export class UnknownLeafTask extends LeafTask { export abstract class LeafWithFileStatDoneFileTask extends LeafWithDoneFileTask { /** - * @returns the list of files that this task depends on. The files are relative to the package directory. + * @returns the list of absolute paths to files that this task depends on. */ protected abstract getInputFiles(): Promise; + /** - * @returns the list of files that this task generates. The files are relative to the package directory. + * @returns the list of absolute paths to files that this task generates. */ protected abstract getOutputFiles(): Promise; diff --git a/build-tools/packages/build-tools/src/index.ts b/build-tools/packages/build-tools/src/index.ts index af4251ce8f6b..83ba7e216a61 100644 --- a/build-tools/packages/build-tools/src/index.ts +++ b/build-tools/packages/build-tools/src/index.ts @@ -4,20 +4,8 @@ */ export { GitRepo } from "./common/gitRepo"; -export { FluidRepo } from "./common/fluidRepo"; -export type { - ITypeValidationConfig, - IFluidBuildConfig, - PackageNamePolicyConfig, - PolicyConfig, - BrokenCompatTypes, - PreviousVersionStyle, - ReleaseNotesSectionName, - ReleaseNotesConfig, - ReleaseNotesSection, - ScriptRequirement, -} from "./common/fluidRepo"; -export { getResolvedFluidRoot, loadFluidBuildConfig } from "./common/fluidUtils"; +export { FluidRepo, type IFluidBuildConfig } from "./common/fluidRepo"; +export { getResolvedFluidRoot, getFluidBuildConfig } from "./common/fluidUtils"; export type { Logger } from "./common/logging"; export { MonoRepo } from "./common/monoRepo"; export { @@ -36,6 +24,10 @@ export { export { getApiExtractorConfigFilePath, getEsLintConfigFilePath } from "./common/taskUtils"; export * as TscUtils from "./common/tscUtils"; +export { + type BrokenCompatTypes, + type ITypeValidationConfig, +} from "./common/typeValidatorConfig"; export { TypeOnly, MinimalType, diff --git a/build-tools/packages/build-tools/src/test/biomeConfig.test.ts b/build-tools/packages/build-tools/src/test/biomeConfig.test.ts new file mode 100644 index 000000000000..e3db5a6a5b99 --- /dev/null +++ b/build-tools/packages/build-tools/src/test/biomeConfig.test.ts @@ -0,0 +1,312 @@ +/*! + * Copyright (c) Microsoft Corporation and contributors. All rights reserved. + * Licensed under the MIT License. + */ + +/* eslint-disable @typescript-eslint/no-non-null-assertion */ +// There are no cases in this file where the values being checked should be undefined, so `!.` is more correct with +// respect to intent than `?.`. + +import assert from "node:assert/strict"; +import path from "node:path"; +import { + BiomeConfigReader, + type BiomeConfigResolved, + getBiomeFormattedFilesFromDirectory, + getSettingValuesFromBiomeConfig, + loadBiomeConfig, +} from "../common/biomeConfig"; +import type { Configuration as BiomeConfigOnDisk } from "../common/biomeConfigTypes"; +import { getResolvedFluidRoot } from "../common/fluidUtils"; +import { GitRepo } from "../common/gitRepo"; +import { testDataPath } from "./init"; + +describe("Biome config loading", () => { + describe("BiomeConfigReader class", () => { + // These variables need to be initialized once for all the tests in this describe block. Defining them outside + // of the before block causes the tests to be skipped. + const testDir = path.resolve(testDataPath, "biome/pkg-b"); + let gitRepo: GitRepo; + before(async () => { + const repoRoot = await getResolvedFluidRoot(true); + gitRepo = new GitRepo(repoRoot); + }); + + it("loads", async () => { + const config = await BiomeConfigReader.create(testDir, gitRepo); + assert(config !== undefined); + }); + + it("has correct formatted files list", async () => { + const config = await BiomeConfigReader.create(testDir, gitRepo); + const expected = [ + path.resolve( + testDataPath, + "biome/pkg-b/include-formatter-added-1/subdirectory/sourceFile2.ts", + ), + path.resolve( + testDataPath, + "biome/pkg-b/include-formatter-added-1/subdirectory/markdownFile2.md", + ), + path.resolve(testDataPath, "biome/pkg-b/include-formatter-added-1/sourceFile.ts"), + path.resolve(testDataPath, "biome/pkg-b/include-formatter-added-1/markdownFile1.md"), + ]; + const { formattedFiles } = config; + assert( + formattedFiles.length === 4, + `expected 4 elements in the array, got ${formattedFiles.length}`, + ); + for (const actual of formattedFiles) { + assert(expected.includes(actual)); + } + }); + + it("returns only files matching files.includes", async () => { + const config = await BiomeConfigReader.create( + path.resolve(testDataPath, "biome/pkg-b/include-md-only.jsonc"), + gitRepo, + ); + const expected = [ + path.resolve( + testDataPath, + "biome/pkg-b/include-formatter-added-1/subdirectory/markdownFile2.md", + ), + path.resolve(testDataPath, "biome/pkg-b/include-formatter-added-1/markdownFile1.md"), + ]; + const { formattedFiles } = config; + assert( + formattedFiles.length === 2, + `expected 2 elements in the array, got ${formattedFiles.length}`, + ); + for (const actual of formattedFiles) { + assert(expected.includes(actual)); + } + }); + }); + + describe("loadConfig", () => { + it("throws on missing config", async () => { + const testFile = path.resolve(testDataPath, "biome/missing.jsonc"); + assert.rejects(async () => await loadBiomeConfig(testFile), Error); + }); + + it("throws on empty config", async () => { + const testFile = path.resolve(testDataPath, "biome/empty.jsonc"); + assert.rejects(async () => await loadBiomeConfig(testFile), Error); + }); + + it("loads single config", async () => { + const testFile = path.resolve(testDataPath, "biome/base.jsonc"); + const actual = await loadBiomeConfig(testFile); + assert.notEqual(actual, undefined); + assert.equal(actual.files!.ignoreUnknown, true); + }); + + it("loads config with multiple extends", async () => { + const testFile = path.resolve(testDataPath, "biome/pkg-b/biome.jsonc"); + const actual = await loadBiomeConfig(testFile); + + assert(actual !== undefined); + + assert(actual.files!.ignoreUnknown === false); + assert(actual.files!.include!.includes("pkg-a-include/**")); + assert(actual.files!.ignore!.includes("pkg-a-ignore/**")); + assert(actual.formatter!.include!.includes("include-formatter-added-1/**")); + assert(actual.formatter!.ignore!.includes("ignore-formatter-added-1/**")); + assert(actual.linter!.include!.includes("include-linter-added-1/**")); + assert(actual.linter!.ignore!.includes("ignore-linter-added-1/**")); + }); + + describe("extends from a single config", () => { + // These variables need to be initialized once for all the tests in this describe block. Defining them outside + // of the before block causes the tests to be skipped. + let testConfig: BiomeConfigOnDisk; + before(async () => { + const testFile = path.resolve(testDataPath, "biome/pkg-a/biome.jsonc"); + testConfig = await loadBiomeConfig(testFile); + }); + + it("top-level property is inherited", async () => { + assert(testConfig !== undefined); + assert(testConfig.files!.ignoreUnknown === true); + }); + + it("files.include is overridden by loaded config", async () => { + assert(testConfig.files!.include!.includes("pkg-a-include/**")); + assert( + testConfig.files!.include!.length === 1, + `expected 1 elements in the array, got ${testConfig.files!.include!.length}`, + ); + }); + + it("files.ignore is overridden by loaded config", async () => { + assert(testConfig.files!.ignore!.includes("pkg-a-ignore/**")); + assert( + testConfig.files!.ignore!.length === 1, + `expected 1 elements in the array, got ${testConfig.files!.ignore!.length}`, + ); + }); + + it("formatter.include is overridden by loaded config", async () => { + const testFile = path.resolve(testDataPath, "biome/pkg-a/biome.jsonc"); + const actual = await loadBiomeConfig(testFile); + + assert(actual.formatter!.include!.includes("include-formatter/**")); + assert( + actual.formatter!.include!.length === 1, + `expected 1 elements in the array, got ${actual.formatter!.include!.length}`, + ); + }); + + it("formatter.ignore is overridden by loaded config", async () => { + const testFile = path.resolve(testDataPath, "biome/pkg-a/biome.jsonc"); + const actual = await loadBiomeConfig(testFile); + + assert(actual.formatter!.ignore!.includes("ignore-formatter/**")); + assert( + actual.formatter!.ignore!.length === 1, + `expected 1 elements in the array, got ${actual.formatter!.ignore!.length}`, + ); + }); + + it("linter.include is overridden by loaded config", async () => { + const testFile = path.resolve(testDataPath, "biome/pkg-a/biome.jsonc"); + const actual = await loadBiomeConfig(testFile); + + assert(actual.linter!.include!.includes("include-linter/**")); + assert( + actual.linter!.include!.length === 1, + `expected 1 elements in the array, got ${actual.linter!.include!.length}`, + ); + }); + + it("linter.ignore is overridden by loaded config", async () => { + const testFile = path.resolve(testDataPath, "biome/pkg-a/biome.jsonc"); + const actual = await loadBiomeConfig(testFile); + + assert(actual.linter!.ignore!.includes("ignore-linter/**")); + assert( + actual.linter!.ignore!.length === 1, + `expected 1 elements in the array, got ${actual.linter!.ignore!.length}`, + ); + }); + }); + }); + + describe("getSettingValuesFromBiomeConfig", () => { + describe("extends from a single config", () => { + // These variables need to be initialized once for all the tests in this describe block. Defining them outside + // of the before block causes the tests to be skipped. + const testFile = path.resolve(testDataPath, "biome/pkg-a/biome.jsonc"); + let testConfig: BiomeConfigResolved; + before(async () => { + testConfig = await loadBiomeConfig(testFile); + }); + + it("formatter ignore settings are merged with root", async () => { + const ignores = await getSettingValuesFromBiomeConfig( + testConfig, + "formatter", + "ignore", + ); + assert(ignores.has("pkg-a-ignore/**")); + assert(ignores.has("ignore-formatter/**")); + assert(ignores.size === 2, `expected 2 items in the set, got ${ignores.size}`); + }); + + it("linter ignore settings are merged with root", async () => { + const ignores = await getSettingValuesFromBiomeConfig(testConfig, "linter", "ignore"); + assert(ignores.has("pkg-a-ignore/**")); + assert(ignores.has("ignore-linter/**")); + assert(ignores.size === 2); + }); + + it("formatter include settings are merged with root", async () => { + const includes = await getSettingValuesFromBiomeConfig( + testConfig, + "formatter", + "include", + ); + assert(includes.has("pkg-a-include/**")); + assert(includes.has("include-formatter/**")); + assert(includes.size === 2); + }); + + it("linter include settings are merged with root", async () => { + const includes = await getSettingValuesFromBiomeConfig( + testConfig, + "linter", + "include", + ); + assert(includes.has("pkg-a-include/**")); + assert(includes.has("include-linter/**")); + assert(includes.size === 2); + }); + }); + }); + + describe("getBiomeFormattedFilesFromDirectory", () => { + describe("extends from a single config", () => { + // These variables need to be initialized once for all the tests in this describe block. Defining them outside + // of the before block causes the tests to be skipped. + const testPath = path.resolve(testDataPath, "biome/pkg-a/"); + let gitRepo: GitRepo; + before(async () => { + const repoRoot = await getResolvedFluidRoot(true); + gitRepo = new GitRepo(repoRoot); + }); + + it("returns correct file set", async () => { + const expected = [ + path.resolve(testDataPath, "biome/pkg-a/pkg-a-include/sourceFile.ts"), + path.resolve( + testDataPath, + "biome/pkg-a/pkg-a-include/include-formatter/formatter.ts", + ), + path.resolve(testDataPath, "biome/pkg-a/pkg-a-include/include-linter/linter.ts"), + path.resolve(testDataPath, "biome/pkg-a/include-formatter/formatter.ts"), + ]; + const formattedFiles = await getBiomeFormattedFilesFromDirectory(testPath, gitRepo); + for (const actual of formattedFiles) { + assert(expected.includes(actual)); + } + assert( + formattedFiles.length === 4, + `expected 4 elements in the array, got ${formattedFiles.length}`, + ); + }); + }); + + describe("extends from multiple configs", () => { + // These variables need to be initialized once for all the tests in this describe block. Defining them outside + // of the before block causes the tests to be skipped. + const testPath = path.resolve(testDataPath, "biome/pkg-a/extended.jsonc"); + let gitRepo: GitRepo; + before(async () => { + const repoRoot = await getResolvedFluidRoot(true); + gitRepo = new GitRepo(repoRoot); + }); + + it("returns correct file set", async () => { + const expected = [ + path.resolve(testDataPath, "biome/pkg-a/pkg-a-include/sourceFile.ts"), + path.resolve(testDataPath, "biome/pkg-a/pkg-a-include/include-linter/linter.ts"), + path.resolve( + testDataPath, + "biome/pkg-a/pkg-a-include/include-formatter/formatter.ts", + ), + path.resolve(testDataPath, "biome/pkg-a/pkg-a-include/pkg-a-ignore/ignoredFile.ts"), + path.resolve(testDataPath, "biome/pkg-a/include-formatter/formatter.ts"), + ]; + const formattedFiles = await getBiomeFormattedFilesFromDirectory(testPath, gitRepo); + for (const actual of formattedFiles) { + assert(expected.includes(actual)); + } + assert( + formattedFiles.length === 5, + `expected 5 elements in the array, got ${formattedFiles.length}`, + ); + }); + }); + }); +}); diff --git a/build-tools/packages/build-tools/src/test/data/biome/add-includes-ignores.jsonc b/build-tools/packages/build-tools/src/test/data/biome/add-includes-ignores.jsonc new file mode 100644 index 000000000000..aa0eede3c307 --- /dev/null +++ b/build-tools/packages/build-tools/src/test/data/biome/add-includes-ignores.jsonc @@ -0,0 +1,11 @@ +{ + "$schema": "../../../../node_modules/@biomejs/biome/configuration_schema.json", + "formatter": { + "include": ["include-formatter-added-1/**"], + "ignore": ["ignore-formatter-added-1/**"] + }, + "linter": { + "include": ["include-linter-added-1/**"], + "ignore": ["ignore-linter-added-1/**"] + } +} diff --git a/build-tools/packages/build-tools/src/test/data/biome/base.jsonc b/build-tools/packages/build-tools/src/test/data/biome/base.jsonc new file mode 100644 index 000000000000..ffe1ff6a342c --- /dev/null +++ b/build-tools/packages/build-tools/src/test/data/biome/base.jsonc @@ -0,0 +1,131 @@ +{ + "$schema": "../../../../node_modules/@biomejs/biome/configuration_schema.json", + "vcs": { + "enabled": true, + "clientKind": "git", + "root": ".", + "useIgnoreFile": true, + "defaultBranch": "main" + }, + "files": { + "ignoreUnknown": true, + + "ignore": [ + // Build output + "**/base-1/*", + "**/base-2/*", + "**/base/*.done.build.log" + ], + + "maxSize": 2097152 + }, + "organizeImports": { + "enabled": false + }, + "linter": { + "enabled": false, + "rules": { + "recommended": true + } + }, + "formatter": { + "enabled": true, + "formatWithErrors": true, + "indentStyle": "tab", + "lineWidth": 95, + "lineEnding": "lf" + }, + "javascript": { + "formatter": { + "arrowParentheses": "always", + "jsxQuoteStyle": "double", + "semicolons": "always", + "trailingCommas": "all", + "quoteProperties": "preserve", + "quoteStyle": "double", + "bracketSpacing": true + } + }, + "json": { + "formatter": { + "indentStyle": "tab" + } + }, + "overrides": [ + { + // @fluid-experimental/tree FORMATTING + // This configuration is used to format the @fluid-experimental/tree package, which uses different settings than + // most projects. This override is needed to ensure that the formatter is applied correctly when run from the root + // of the repo. + // + // This configuration should be kept up-to-date with the settings in `experimental/dds/tree/biome.jsonc`. + "include": ["experimental/dds/tree/**"], + "formatter": { + "lineWidth": 120 + }, + "javascript": { + "formatter": { + "jsxQuoteStyle": "single", + "trailingCommas": "es5", + "quoteStyle": "single" + } + } + }, + { + // JSONC WITHOUT TRAILING COMMAS JSONC is not a standard, and support for trailing commas is not universal. For + // simplicity and safety, we parse most JSONC files in a liberal way -- allowing comments and trailing commas, but + // format them conservatively without trailing commas. + // + // See also: https://github.com/microsoft/vscode/issues/102061 + "include": [ + "**/*.jsonc", + + // Tools reading api-extractor config files do not consistently support trailing commas. + "**/api-extractor*.json", + + // Tools reading tsdoc config files do not consistently support trailing commas. + "**/tsdoc*.json" + ], + "json": { + "parser": { + "allowComments": true, + "allowTrailingCommas": true + }, + "formatter": { + "trailingCommas": "none" + } + } + }, + { + // JSONC WITH TRAILING COMMAS + // These JSONC files are known to support trailing commas. + "include": [ + // vscode config files all support trailing commas. + "**/.vscode/*.json", + + // tsconfig files support trailing commas. + "**/tsconfig*.json" + ], + "json": { + "parser": { + "allowComments": true, + "allowTrailingCommas": true + }, + "formatter": { + "trailingCommas": "all" + } + } + }, + { + // PACKAGE.JSON + // These settings are used to format package.json files in the way npm itself does, with the exception of using + // tabs instead of spaces. + "include": ["**/package.json"], + "json": { + "formatter": { + "lineWidth": 1 + } + } + } + ] +} diff --git a/build-tools/packages/build-tools/src/test/data/biome/empty.jsonc b/build-tools/packages/build-tools/src/test/data/biome/empty.jsonc new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/build-tools/packages/build-tools/src/test/data/biome/pkg-a/biome.jsonc b/build-tools/packages/build-tools/src/test/data/biome/pkg-a/biome.jsonc new file mode 100644 index 000000000000..4e8707c74300 --- /dev/null +++ b/build-tools/packages/build-tools/src/test/data/biome/pkg-a/biome.jsonc @@ -0,0 +1,16 @@ +{ + "$schema": "../../../../../node_modules/@biomejs/biome/configuration_schema.json", + "extends": ["../base.jsonc"], + "files": { + "include": ["pkg-a-include/**"], + "ignore": ["pkg-a-ignore/**"] + }, + "formatter": { + "include": ["include-formatter/**"], + "ignore": ["ignore-formatter/**"] + }, + "linter": { + "include": ["include-linter/**"], + "ignore": ["ignore-linter/**"] + } +} diff --git a/build-tools/packages/build-tools/src/test/data/biome/pkg-a/extended.jsonc b/build-tools/packages/build-tools/src/test/data/biome/pkg-a/extended.jsonc new file mode 100644 index 000000000000..c17f4599ea53 --- /dev/null +++ b/build-tools/packages/build-tools/src/test/data/biome/pkg-a/extended.jsonc @@ -0,0 +1,9 @@ +{ + "extends": ["biome.jsonc"], + "files": { + "ignore": [] + }, + "formatter": { + "ignore": [] + } +} diff --git a/build-tools/packages/build-tools/src/test/data/biome/pkg-a/include-formatter/formatter.ts b/build-tools/packages/build-tools/src/test/data/biome/pkg-a/include-formatter/formatter.ts new file mode 100644 index 000000000000..1e7d691731cc --- /dev/null +++ b/build-tools/packages/build-tools/src/test/data/biome/pkg-a/include-formatter/formatter.ts @@ -0,0 +1,6 @@ +/*! + * Copyright (c) Microsoft Corporation and contributors. All rights reserved. + * Licensed under the MIT License. + */ + +export const testValue = 1; diff --git a/build-tools/packages/build-tools/src/test/data/biome/pkg-a/pkg-a-ignore/ignoredFile.ts b/build-tools/packages/build-tools/src/test/data/biome/pkg-a/pkg-a-ignore/ignoredFile.ts new file mode 100644 index 000000000000..1e7d691731cc --- /dev/null +++ b/build-tools/packages/build-tools/src/test/data/biome/pkg-a/pkg-a-ignore/ignoredFile.ts @@ -0,0 +1,6 @@ +/*! + * Copyright (c) Microsoft Corporation and contributors. All rights reserved. + * Licensed under the MIT License. + */ + +export const testValue = 1; diff --git a/build-tools/packages/build-tools/src/test/data/biome/pkg-a/pkg-a-include/include-formatter/formatter.ts b/build-tools/packages/build-tools/src/test/data/biome/pkg-a/pkg-a-include/include-formatter/formatter.ts new file mode 100644 index 000000000000..1e7d691731cc --- /dev/null +++ b/build-tools/packages/build-tools/src/test/data/biome/pkg-a/pkg-a-include/include-formatter/formatter.ts @@ -0,0 +1,6 @@ +/*! + * Copyright (c) Microsoft Corporation and contributors. All rights reserved. + * Licensed under the MIT License. + */ + +export const testValue = 1; diff --git a/build-tools/packages/build-tools/src/test/data/biome/pkg-a/pkg-a-include/include-linter/linter.ts b/build-tools/packages/build-tools/src/test/data/biome/pkg-a/pkg-a-include/include-linter/linter.ts new file mode 100644 index 000000000000..1e7d691731cc --- /dev/null +++ b/build-tools/packages/build-tools/src/test/data/biome/pkg-a/pkg-a-include/include-linter/linter.ts @@ -0,0 +1,6 @@ +/*! + * Copyright (c) Microsoft Corporation and contributors. All rights reserved. + * Licensed under the MIT License. + */ + +export const testValue = 1; diff --git a/build-tools/packages/build-tools/src/test/data/biome/pkg-a/pkg-a-include/pkg-a-ignore/ignoredFile.ts b/build-tools/packages/build-tools/src/test/data/biome/pkg-a/pkg-a-include/pkg-a-ignore/ignoredFile.ts new file mode 100644 index 000000000000..1e7d691731cc --- /dev/null +++ b/build-tools/packages/build-tools/src/test/data/biome/pkg-a/pkg-a-include/pkg-a-ignore/ignoredFile.ts @@ -0,0 +1,6 @@ +/*! + * Copyright (c) Microsoft Corporation and contributors. All rights reserved. + * Licensed under the MIT License. + */ + +export const testValue = 1; diff --git a/build-tools/packages/build-tools/src/test/data/biome/pkg-a/pkg-a-include/sourceFile.ts b/build-tools/packages/build-tools/src/test/data/biome/pkg-a/pkg-a-include/sourceFile.ts new file mode 100644 index 000000000000..1e7d691731cc --- /dev/null +++ b/build-tools/packages/build-tools/src/test/data/biome/pkg-a/pkg-a-include/sourceFile.ts @@ -0,0 +1,6 @@ +/*! + * Copyright (c) Microsoft Corporation and contributors. All rights reserved. + * Licensed under the MIT License. + */ + +export const testValue = 1; diff --git a/build-tools/packages/build-tools/src/test/data/biome/pkg-b/biome.jsonc b/build-tools/packages/build-tools/src/test/data/biome/pkg-b/biome.jsonc new file mode 100644 index 000000000000..f19a5a908350 --- /dev/null +++ b/build-tools/packages/build-tools/src/test/data/biome/pkg-b/biome.jsonc @@ -0,0 +1,7 @@ +{ + "$schema": "../../../../../node_modules/@biomejs/biome/configuration_schema.json", + "extends": ["../pkg-a/biome.jsonc", "../add-includes-ignores.jsonc"], + "files": { + "ignoreUnknown": false + } +} diff --git a/build-tools/packages/build-tools/src/test/data/biome/pkg-b/ignore-formatter-added-1/ignoredFile.ts b/build-tools/packages/build-tools/src/test/data/biome/pkg-b/ignore-formatter-added-1/ignoredFile.ts new file mode 100644 index 000000000000..1e7d691731cc --- /dev/null +++ b/build-tools/packages/build-tools/src/test/data/biome/pkg-b/ignore-formatter-added-1/ignoredFile.ts @@ -0,0 +1,6 @@ +/*! + * Copyright (c) Microsoft Corporation and contributors. All rights reserved. + * Licensed under the MIT License. + */ + +export const testValue = 1; diff --git a/build-tools/packages/build-tools/src/test/data/biome/pkg-b/include-formatter-added-1/markdownFile1.md b/build-tools/packages/build-tools/src/test/data/biome/pkg-b/include-formatter-added-1/markdownFile1.md new file mode 100644 index 000000000000..524acfffa760 --- /dev/null +++ b/build-tools/packages/build-tools/src/test/data/biome/pkg-b/include-formatter-added-1/markdownFile1.md @@ -0,0 +1 @@ +Test file diff --git a/build-tools/packages/build-tools/src/test/data/biome/pkg-b/include-formatter-added-1/sourceFile.ts b/build-tools/packages/build-tools/src/test/data/biome/pkg-b/include-formatter-added-1/sourceFile.ts new file mode 100644 index 000000000000..1e7d691731cc --- /dev/null +++ b/build-tools/packages/build-tools/src/test/data/biome/pkg-b/include-formatter-added-1/sourceFile.ts @@ -0,0 +1,6 @@ +/*! + * Copyright (c) Microsoft Corporation and contributors. All rights reserved. + * Licensed under the MIT License. + */ + +export const testValue = 1; diff --git a/build-tools/packages/build-tools/src/test/data/biome/pkg-b/include-formatter-added-1/subdirectory/markdownFile2.md b/build-tools/packages/build-tools/src/test/data/biome/pkg-b/include-formatter-added-1/subdirectory/markdownFile2.md new file mode 100644 index 000000000000..524acfffa760 --- /dev/null +++ b/build-tools/packages/build-tools/src/test/data/biome/pkg-b/include-formatter-added-1/subdirectory/markdownFile2.md @@ -0,0 +1 @@ +Test file diff --git a/build-tools/packages/build-tools/src/test/data/biome/pkg-b/include-formatter-added-1/subdirectory/sourceFile2.ts b/build-tools/packages/build-tools/src/test/data/biome/pkg-b/include-formatter-added-1/subdirectory/sourceFile2.ts new file mode 100644 index 000000000000..1e7d691731cc --- /dev/null +++ b/build-tools/packages/build-tools/src/test/data/biome/pkg-b/include-formatter-added-1/subdirectory/sourceFile2.ts @@ -0,0 +1,6 @@ +/*! + * Copyright (c) Microsoft Corporation and contributors. All rights reserved. + * Licensed under the MIT License. + */ + +export const testValue = 1; diff --git a/build-tools/packages/build-tools/src/test/data/biome/pkg-b/include-md-only.jsonc b/build-tools/packages/build-tools/src/test/data/biome/pkg-b/include-md-only.jsonc new file mode 100644 index 000000000000..da01ad4e5c6d --- /dev/null +++ b/build-tools/packages/build-tools/src/test/data/biome/pkg-b/include-md-only.jsonc @@ -0,0 +1,7 @@ +{ + "$schema": "../../../../node_modules/@biomejs/biome/configuration_schema.json", + "extends": ["../pkg-a/biome.jsonc"], + "files": { + "include": ["*.md"] + } +} diff --git a/build-tools/packages/build-tools/src/test/data/biome/sourceFile2.ts b/build-tools/packages/build-tools/src/test/data/biome/sourceFile2.ts new file mode 100644 index 000000000000..1e7d691731cc --- /dev/null +++ b/build-tools/packages/build-tools/src/test/data/biome/sourceFile2.ts @@ -0,0 +1,6 @@ +/*! + * Copyright (c) Microsoft Corporation and contributors. All rights reserved. + * Licensed under the MIT License. + */ + +export const testValue = 1; diff --git a/build-tools/packages/build-tools/src/test/init.ts b/build-tools/packages/build-tools/src/test/init.ts new file mode 100644 index 000000000000..928af1985569 --- /dev/null +++ b/build-tools/packages/build-tools/src/test/init.ts @@ -0,0 +1,11 @@ +/*! + * Copyright (c) Microsoft Corporation and contributors. All rights reserved. + * Licensed under the MIT License. + */ + +import path from "node:path"; + +/** + * Path to the test data. It's rooted two directories up because the tests get executed from dist/. + */ +export const testDataPath = path.resolve(__dirname, "../../src/test/data"); diff --git a/build-tools/packages/build-tools/src/test/npmPackage.test.ts b/build-tools/packages/build-tools/src/test/npmPackage.test.ts index 5bbfaa1f03b4..e2d36366382a 100644 --- a/build-tools/packages/build-tools/src/test/npmPackage.test.ts +++ b/build-tools/packages/build-tools/src/test/npmPackage.test.ts @@ -11,11 +11,7 @@ import { readPackageJsonAndIndent, updatePackageJsonFile, } from "../common/npmPackage"; - -/** - * Path to the test data. It's rooted two directories up because the tests get executed from dist/. - */ -const testDataPath = path.resolve(__dirname, "../../src/test/data"); +import { testDataPath } from "./init"; /** * A transformer function that does nothing. diff --git a/build-tools/packages/bundle-size-tools/package.json b/build-tools/packages/bundle-size-tools/package.json index 43dc9a70045a..3c8dfc5e0c7a 100644 --- a/build-tools/packages/bundle-size-tools/package.json +++ b/build-tools/packages/bundle-size-tools/package.json @@ -1,6 +1,6 @@ { "name": "@fluidframework/bundle-size-tools", - "version": "0.43.0", + "version": "0.45.0", "description": "Utility for analyzing bundle size regressions", "homepage": "https://fluidframework.com", "repository": { diff --git a/build-tools/packages/version-tools/package.json b/build-tools/packages/version-tools/package.json index 93319cf1bfd5..f8d80385a8c4 100644 --- a/build-tools/packages/version-tools/package.json +++ b/build-tools/packages/version-tools/package.json @@ -1,6 +1,6 @@ { "name": "@fluid-tools/version-tools", - "version": "0.43.0", + "version": "0.45.0", "description": "Versioning tools for Fluid Framework", "homepage": "https://fluidframework.com", "repository": { diff --git a/build-tools/packages/version-tools/src/bumpTypes.ts b/build-tools/packages/version-tools/src/bumpTypes.ts index 19d9a78772a8..4a4a3a071636 100644 --- a/build-tools/packages/version-tools/src/bumpTypes.ts +++ b/build-tools/packages/version-tools/src/bumpTypes.ts @@ -69,7 +69,7 @@ export function isWorkspaceRange(r: unknown): r is WorkspaceRange { } /** - * A type represeting the strings we consider valid for interdependencies - dependencies between packages within the + * A type representing the strings we consider valid for interdependencies - dependencies between packages within the * same release group. */ export type InterdependencyRange = diff --git a/build-tools/pnpm-lock.yaml b/build-tools/pnpm-lock.yaml index a772af04fdd1..e9ee98541041 100644 --- a/build-tools/pnpm-lock.yaml +++ b/build-tools/pnpm-lock.yaml @@ -138,6 +138,9 @@ importers: change-case: specifier: ^3.1.0 version: 3.1.0 + cosmiconfig: + specifier: ^8.3.6 + version: 8.3.6(typescript@5.4.5) danger: specifier: ^11.3.0 version: 11.3.0 @@ -364,9 +367,6 @@ importers: packages/build-tools: dependencies: - '@fluid-tools/version-tools': - specifier: workspace:~ - version: link:../version-tools '@manypkg/get-packages': specifier: ^2.2.0 version: 2.2.0 @@ -377,8 +377,8 @@ importers: specifier: ^2.4.2 version: 2.4.2 cosmiconfig: - specifier: ^8.2.0 - version: 8.2.0 + specifier: ^8.3.6 + version: 8.3.6(typescript@5.4.5) date-fns: specifier: ^2.30.0 version: 2.30.0 @@ -409,6 +409,9 @@ importers: lodash.isequal: specifier: ^4.5.0 version: 4.5.0 + multimatch: + specifier: ^5.0.0 + version: 5.0.0 picomatch: specifier: ^2.3.1 version: 2.3.1 @@ -421,6 +424,9 @@ importers: sort-package-json: specifier: 1.57.0 version: 1.57.0 + ts-deepmerge: + specifier: ^7.0.0 + version: 7.0.1 ts-morph: specifier: ^22.0.0 version: 22.0.0 @@ -482,6 +488,9 @@ importers: eslint: specifier: ~8.57.0 version: 8.57.0 + json-schema-to-typescript: + specifier: ^15.0.0 + version: 15.0.0 mocha: specifier: ^10.2.0 version: 10.2.0 @@ -660,6 +669,15 @@ packages: /@andrewbranch/untar.js@1.0.3: resolution: {integrity: sha512-Jh15/qVmrLGhkKJBdXlK1+9tY4lZruYjsgkDFj08ZmDiWVBLJcqkok7Z0/R0In+i1rScBpJlSvrTS2Lm41Pbnw==} + /@apidevtools/json-schema-ref-parser@11.7.0: + resolution: {integrity: sha512-pRrmXMCwnmrkS3MLgAIW5dXRzeTv6GLjkjb4HmxNnvAKXN1Nfzp4KmGADBQvlVUcqi+a5D+hfGDLLnd5NnYxog==} + engines: {node: '>= 16'} + dependencies: + '@jsdevtools/ono': 7.1.3 + '@types/json-schema': 7.0.15 + js-yaml: 4.1.0 + dev: true + /@babel/code-frame@7.18.6: resolution: {integrity: sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q==} engines: {node: '>=6.9.0'} @@ -907,8 +925,8 @@ packages: '@commitlint/types': 17.4.4 '@types/node': 18.18.7 chalk: 4.1.2 - cosmiconfig: 8.2.0 - cosmiconfig-typescript-loader: 4.2.0(@types/node@18.18.7)(cosmiconfig@8.2.0)(ts-node@10.9.1)(typescript@5.4.5) + cosmiconfig: 8.3.6(typescript@5.4.5) + cosmiconfig-typescript-loader: 4.2.0(@types/node@18.18.7)(cosmiconfig@8.3.6)(ts-node@10.9.1)(typescript@5.4.5) lodash.isplainobject: 4.0.6 lodash.merge: 4.6.2 lodash.uniq: 4.5.0 @@ -931,8 +949,8 @@ packages: '@commitlint/types': 18.1.0 '@types/node': 18.18.7 chalk: 4.1.2 - cosmiconfig: 8.2.0 - cosmiconfig-typescript-loader: 5.0.0(@types/node@18.18.7)(cosmiconfig@8.2.0)(typescript@5.4.5) + cosmiconfig: 8.3.6(typescript@5.4.5) + cosmiconfig-typescript-loader: 5.0.0(@types/node@18.18.7)(cosmiconfig@8.3.6)(typescript@5.4.5) lodash.isplainobject: 4.0.6 lodash.merge: 4.6.2 lodash.uniq: 4.5.0 @@ -1201,7 +1219,7 @@ packages: '@rushstack/node-core-library': 3.59.5(@types/node@18.18.6) async: 3.2.4 chalk: 2.4.2 - cosmiconfig: 8.2.0 + cosmiconfig: 8.3.6(typescript@5.4.5) danger: 11.3.0 date-fns: 2.30.0 debug: 4.3.4(supports-color@8.1.1) @@ -1628,6 +1646,10 @@ packages: '@jridgewell/sourcemap-codec': 1.4.14 dev: true + /@jsdevtools/ono@7.1.3: + resolution: {integrity: sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==} + dev: true + /@kwsites/file-exists@1.1.1: resolution: {integrity: sha512-m9/5YGR18lIwxSFDwfE3oA7bWuq9kdau6ugN4H2rJeyhFQZcG9AgSHkQtSD15a8WvTgfz9aikZMrKPHvbpqFiw==} dependencies: @@ -2444,6 +2466,10 @@ packages: /@types/json-schema@7.0.14: resolution: {integrity: sha512-U3PUjAudAdJBeC2pgN8uTIKgxrb4nlDF3SF0++EldXQvQBGkpFZMSnwQiIoDU77tv45VgNkl/L4ouD+rEomujw==} + /@types/json-schema@7.0.15: + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + dev: true + /@types/json5@0.0.29: resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} dev: true @@ -2469,6 +2495,10 @@ packages: resolution: {integrity: sha512-Hwx9EUgdwf2GLarOjQp5ZH8ZmblzcbTBC2wtQWNKARBSxM9ezRIAUpeDTgoQRAFB0+8CNWXVA9+MaSOzOF3nPg==} dev: true + /@types/lodash@4.17.7: + resolution: {integrity: sha512-8wTvZawATi/lsmNu10/j2hk1KEP0IvjubqPE3cu1Xz7xfXXt5oCq3SNUz4fMIP4XGF9Ky+Ue2tBA3hcS7LSBlA==} + dev: true + /@types/mdast@4.0.4: resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==} dependencies: @@ -2801,7 +2831,7 @@ packages: dependencies: '@typescript-eslint/types': 5.59.11 '@typescript-eslint/visitor-keys': 5.59.11 - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.5(supports-color@8.1.1) globby: 11.1.0 is-glob: 4.0.3 semver: 7.6.2 @@ -3284,6 +3314,11 @@ packages: call-bind: 1.0.2 is-array-buffer: 3.0.2 + /array-differ@3.0.0: + resolution: {integrity: sha512-THtfYS6KtME/yIAhKjZ2ul7XI96lQGHRputJQHO80LAWQnuGP4iCIN8vdMRboGbIEYBwU33q8Tch1os2+X0kMg==} + engines: {node: '>=8'} + dev: false + /array-ify@1.0.0: resolution: {integrity: sha512-c5AMf34bKdvPhQ7tBGhqkgKNUzMr4WUs+WDtC2ZUGOUncbxKMTvqxYctiseW3+L4bA8ec+GcZ6/A/FW4m8ukng==} dev: true @@ -3360,6 +3395,11 @@ packages: engines: {node: '>=0.10.0'} dev: true + /arrify@2.0.1: + resolution: {integrity: sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==} + engines: {node: '>=8'} + dev: false + /assertion-error@1.1.0: resolution: {integrity: sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==} dev: true @@ -3853,6 +3893,17 @@ packages: timers-ext: 0.1.7 dev: true + /cli-color@2.0.4: + resolution: {integrity: sha512-zlnpg0jNcibNrO7GG9IeHH7maWFeCz+Ja1wx/7tZNU5ASSSSZ+/qZciM0/LHCYxSdqv5h2sdbQ/PXYdOuetXvA==} + engines: {node: '>=0.10'} + dependencies: + d: 1.0.1 + es5-ext: 0.10.64 + es6-iterator: 2.0.3 + memoizee: 0.4.15 + timers-ext: 0.1.7 + dev: true + /cli-cursor@2.1.0: resolution: {integrity: sha512-8lgKz8LmCRYZZQDpRyT2m5rKJ08TnU4tR9FFFW2rxpxR1FzWi4PQ/NfyODchAatHaUgnSPVcx/R5w6NuTBzFiw==} engines: {node: '>=4'} @@ -4303,7 +4354,7 @@ packages: /core-util-is@1.0.3: resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} - /cosmiconfig-typescript-loader@4.2.0(@types/node@18.18.7)(cosmiconfig@8.2.0)(ts-node@10.9.1)(typescript@5.4.5): + /cosmiconfig-typescript-loader@4.2.0(@types/node@18.18.7)(cosmiconfig@8.3.6)(ts-node@10.9.1)(typescript@5.4.5): resolution: {integrity: sha512-NkANeMnaHrlaSSlpKGyvn2R4rqUDeE/9E5YHx+b4nwo0R8dZyAqcih8/gxpCZvqWP9Vf6xuLpMSzSgdVEIM78g==} engines: {node: '>=12', npm: '>=6'} peerDependencies: @@ -4316,12 +4367,12 @@ packages: optional: true dependencies: '@types/node': 18.18.7 - cosmiconfig: 8.2.0 + cosmiconfig: 8.3.6(typescript@5.4.5) ts-node: 10.9.1(@types/node@18.18.7)(typescript@5.4.5) typescript: 5.4.5 dev: true - /cosmiconfig-typescript-loader@5.0.0(@types/node@18.18.7)(cosmiconfig@8.2.0)(typescript@5.4.5): + /cosmiconfig-typescript-loader@5.0.0(@types/node@18.18.7)(cosmiconfig@8.3.6)(typescript@5.4.5): resolution: {integrity: sha512-+8cK7jRAReYkMwMiG+bxhcNKiHJDM6bR9FD/nGBXOWdMLuYawjF5cGrtLilJ+LGd3ZjCXnJjR5DkfWPoIVlqJA==} engines: {node: '>=v16'} requiresBuild: true @@ -4334,7 +4385,7 @@ packages: optional: true dependencies: '@types/node': 18.18.7 - cosmiconfig: 8.2.0 + cosmiconfig: 8.3.6(typescript@5.4.5) jiti: 1.20.0 typescript: 5.4.5 dev: true @@ -4350,14 +4401,20 @@ packages: path-type: 4.0.0 dev: true - /cosmiconfig@8.2.0: - resolution: {integrity: sha512-3rTMnFJA1tCOPwRxtgF4wd7Ab2qvDbL8jX+3smjIbS4HlZBagTlpERbdN7iAbWlrfxE3M8c27kTwTawQ7st+OQ==} + /cosmiconfig@8.3.6(typescript@5.4.5): + resolution: {integrity: sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==} engines: {node: '>=14'} + peerDependencies: + typescript: '>=4.9.5' + peerDependenciesMeta: + typescript: + optional: true dependencies: import-fresh: 3.3.0 js-yaml: 4.1.0 parse-json: 5.2.0 path-type: 4.0.0 + typescript: 5.4.5 /create-require@1.1.1: resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==} @@ -4430,7 +4487,7 @@ packages: /d@1.0.1: resolution: {integrity: sha512-m62ShEObQ39CfralilEQRjH6oAMtNCV1xJyEx5LpRYUVN+EviphDgUc/F3hnYbADmkiNs67Y+3ylmlG7Lnu+FA==} dependencies: - es5-ext: 0.10.62 + es5-ext: 0.10.64 type: 1.2.0 dev: true @@ -4905,11 +4962,22 @@ packages: next-tick: 1.1.0 dev: true + /es5-ext@0.10.64: + resolution: {integrity: sha512-p2snDhiLaXe6dahss1LddxqEm+SkuDvV8dnIQG0MWjyHpcMNfXKPE+/Cc0y+PhxJX3A4xGNeFCj5oc0BUh6deg==} + engines: {node: '>=0.10'} + requiresBuild: true + dependencies: + es6-iterator: 2.0.3 + es6-symbol: 3.1.3 + esniff: 2.0.1 + next-tick: 1.1.0 + dev: true + /es6-iterator@2.0.3: resolution: {integrity: sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g==} dependencies: d: 1.0.1 - es5-ext: 0.10.62 + es5-ext: 0.10.64 es6-symbol: 3.1.3 dev: true @@ -5484,6 +5552,16 @@ packages: - supports-color dev: true + /esniff@2.0.1: + resolution: {integrity: sha512-kTUIGKQ/mDPFoJ0oVfcmyJn4iBDRptjNVIzwIFR7tqWXdVI9xfA2RMwY/gbSpJG3lkdWNEjLap/NqVHZiJsdfg==} + engines: {node: '>=0.10'} + dependencies: + d: 1.0.1 + es5-ext: 0.10.64 + event-emitter: 0.3.5 + type: 2.7.2 + dev: true + /espree@9.6.1: resolution: {integrity: sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -6087,7 +6165,7 @@ packages: dependencies: foreground-child: 3.1.1 jackspeak: 2.3.6 - minimatch: 9.0.4 + minimatch: 9.0.5 minipass: 7.0.4 path-scurry: 1.10.2 @@ -6517,7 +6595,7 @@ packages: resolution: {integrity: sha512-C7FfFoTA+bI10qfeydT8aZbvr91vAEU+2W5BZUlzPec47oNb07SsOfwYrtxuvOYdUApPP/Qlh4DtAO51Ekk2QA==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} dependencies: - minimatch: 9.0.4 + minimatch: 9.0.5 /ignore@5.2.4: resolution: {integrity: sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==} @@ -7057,6 +7135,25 @@ packages: dependencies: jju: 1.4.0 + /json-schema-to-typescript@15.0.0: + resolution: {integrity: sha512-gOX3cJB4eL1ztMc3WUh569ubRcKnr8MnYk++6+/WaaN4bufGHSR6EcbUbvLZgirPQOfvni5SSGkRx0pYloYU8A==} + engines: {node: '>=16.0.0'} + hasBin: true + dependencies: + '@apidevtools/json-schema-ref-parser': 11.7.0 + '@types/json-schema': 7.0.15 + '@types/lodash': 4.17.7 + cli-color: 2.0.4 + glob: 10.3.12 + is-glob: 4.0.3 + js-yaml: 4.1.0 + lodash: 4.17.21 + minimist: 1.2.8 + mkdirp: 3.0.1 + node-fetch: 3.3.2 + prettier: 3.2.5 + dev: true + /json-schema-traverse@0.4.1: resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} @@ -7715,7 +7812,7 @@ packages: resolution: {integrity: sha512-UBWmJpLZd5STPm7PMUlOw/TSy972M+z8gcyQ5veOnSDRREz/0bmpyTfKt3/51DhEBqCZQn1udM/5flcSPYhkdQ==} dependencies: d: 1.0.1 - es5-ext: 0.10.62 + es5-ext: 0.10.64 es6-weak-map: 2.0.3 event-emitter: 0.3.5 is-promise: 2.2.2 @@ -8303,6 +8400,17 @@ packages: int64-buffer: 0.1.10 isarray: 1.0.0 + /multimatch@5.0.0: + resolution: {integrity: sha512-ypMKuglUrZUD99Tk2bUQ+xNQj43lPEfAeX2o9cTteAmShXy2VHDJpuwu1o0xqoKCt9jLVAvwyFKdLTPXKAfJyA==} + engines: {node: '>=10'} + dependencies: + '@types/minimatch': 3.0.5 + array-differ: 3.0.0 + array-union: 2.1.0 + arrify: 2.0.1 + minimatch: 3.1.2 + dev: false + /mute-stream@0.0.7: resolution: {integrity: sha512-r65nCZhrbXXb6dXOACihYApHw2Q6pV0M3V0PSxd74N0+D8nzAdEAITq2oAjA1jVnKI+tGvEBUpqiMh0+rW6zDQ==} dev: true @@ -10435,7 +10543,7 @@ packages: /timers-ext@0.1.7: resolution: {integrity: sha512-b85NUNzTSdodShTIbky6ZF02e8STtVVfD+fu4aXXShEELpozH+bCpJLYMPZbsABN2wDH7fJpqIoXxJpzbf0NqQ==} dependencies: - es5-ext: 0.10.62 + es5-ext: 0.10.64 next-tick: 1.1.0 dev: true @@ -10496,6 +10604,11 @@ packages: typescript: 5.4.5 dev: true + /ts-deepmerge@7.0.1: + resolution: {integrity: sha512-JBFCmNenZdUCc+TRNCtXVM6N8y/nDQHAcpj5BlwXG/gnogjam1NunulB9ia68mnqYI446giMfpqeBFFkOleh+g==} + engines: {node: '>=14.13.1'} + dev: false + /ts-morph@20.0.0: resolution: {integrity: sha512-JVmEJy2Wow5n/84I3igthL9sudQ8qzjh/6i4tmYCm6IqYyKFlNbJZi7oBdjyqcWSWYRu3CtL0xbT6fS03ESZIg==} dependencies: @@ -10624,7 +10737,7 @@ packages: engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} dependencies: '@tufjs/models': 1.0.4 - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.5(supports-color@8.1.1) make-fetch-happen: 11.1.1 transitivePeerDependencies: - supports-color diff --git a/common/build/eslint-config-fluid/CHANGELOG.md b/common/build/eslint-config-fluid/CHANGELOG.md index add7f86d56ee..ecc9e4b25867 100644 --- a/common/build/eslint-config-fluid/CHANGELOG.md +++ b/common/build/eslint-config-fluid/CHANGELOG.md @@ -2,6 +2,10 @@ ## [5.4.0](https://github.com/microsoft/FluidFramework/releases/tag/eslint-config-fluid_v5.4.0) +### New no-unchecked-record-access rule + +Enabled new no-unchecked-record-access rule to enforce safe property access on index signature types. + ### Disabled rules The following rules have been disabled in all configs because they conflict with formatter settings: diff --git a/common/build/eslint-config-fluid/minimal-deprecated.js b/common/build/eslint-config-fluid/minimal-deprecated.js index 71cb080eee97..8399710a8db7 100644 --- a/common/build/eslint-config-fluid/minimal-deprecated.js +++ b/common/build/eslint-config-fluid/minimal-deprecated.js @@ -8,6 +8,11 @@ */ const permittedImports = [ // Within Fluid Framework allow import of '/internal' from other FF packages. + "@fluid-example/*/internal", + "@fluid-experimental/*/internal", + "@fluid-internal/*/internal", + "@fluid-private/*/internal", + "@fluid-tools/*/internal", "@fluidframework/*/internal", // Experimental package APIs and exports are unknown, so allow any imports from them. diff --git a/common/build/eslint-config-fluid/package.json b/common/build/eslint-config-fluid/package.json index 76711224c515..08abb8464c4e 100644 --- a/common/build/eslint-config-fluid/package.json +++ b/common/build/eslint-config-fluid/package.json @@ -28,7 +28,7 @@ "test": "echo TODO: add tests" }, "dependencies": { - "@fluid-internal/eslint-plugin-fluid": "^0.1.1", + "@fluid-internal/eslint-plugin-fluid": "^0.1.2", "@microsoft/tsdoc": "^0.14.2", "@rushstack/eslint-patch": "~1.4.0", "@rushstack/eslint-plugin": "~0.13.0", diff --git a/common/build/eslint-config-fluid/pnpm-lock.yaml b/common/build/eslint-config-fluid/pnpm-lock.yaml index 28c178d22540..2a4c87eb4426 100644 --- a/common/build/eslint-config-fluid/pnpm-lock.yaml +++ b/common/build/eslint-config-fluid/pnpm-lock.yaml @@ -9,8 +9,8 @@ importers: .: dependencies: '@fluid-internal/eslint-plugin-fluid': - specifier: ^0.1.1 - version: 0.1.1(eslint@8.55.0)(typescript@5.1.6) + specifier: ^0.1.2 + version: 0.1.2(eslint@8.55.0)(typescript@5.1.6) '@microsoft/tsdoc': specifier: ^0.14.2 version: 0.14.2 @@ -168,12 +168,12 @@ packages: resolution: {integrity: sha512-qQfo2mxH5yVom1kacMtZZJFVdW+E70mqHMJvVg6WTLo+VBuQJ4TojZlfWBjK0ve5BdEeNAVxOsl/nvNMpJOaJA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - /@fluid-internal/eslint-plugin-fluid@0.1.1(eslint@8.55.0)(typescript@5.1.6): - resolution: {integrity: sha512-7CNeAjn81BPvq/BKc1nQo/6HUZXg4KUAglFuCX6HFCnpGPrDLdm7cdkrGyA1tExB1EGnCAPFzVNbSqSYcwJnag==} + /@fluid-internal/eslint-plugin-fluid@0.1.2(eslint@8.55.0)(typescript@5.1.6): + resolution: {integrity: sha512-E7LF4ukpCoyZcxpDUQz0edXsKllbh4m8NAdiug6sSI1KIIQFwtq5vvW3kQ0Op5xA9w10T6crfcvmuAzdP84UGg==} dependencies: '@microsoft/tsdoc': 0.14.2 - '@typescript-eslint/parser': 6.7.2(eslint@8.55.0)(typescript@5.1.6) - ts-morph: 20.0.0 + '@typescript-eslint/parser': 6.21.0(eslint@8.55.0)(typescript@5.1.6) + ts-morph: 22.0.0 transitivePeerDependencies: - eslint - supports-color @@ -352,12 +352,12 @@ packages: - supports-color dev: true - /@ts-morph/common@0.21.0: - resolution: {integrity: sha512-ES110Mmne5Vi4ypUKrtVQfXFDtCsDXiUiGxF6ILVlE90dDD4fdpC1LSjydl/ml7xJWKSDZwUYD2zkOePMSrPBA==} + /@ts-morph/common@0.23.0: + resolution: {integrity: sha512-m7Lllj9n/S6sOkCkRftpM7L24uvmfXQFedlW/4hENcuJH1HHm9u5EgxZb9uVjQSCGrbBWBkOGgcTxNg36r6ywA==} dependencies: - fast-glob: 3.3.1 - minimatch: 7.4.6 - mkdirp: 2.1.6 + fast-glob: 3.3.2 + minimatch: 9.0.5 + mkdirp: 3.0.1 path-browserify: 1.0.1 dev: false @@ -482,6 +482,27 @@ packages: - typescript dev: false + /@typescript-eslint/parser@6.21.0(eslint@8.55.0)(typescript@5.1.6): + resolution: {integrity: sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + eslint: ^7.0.0 || ^8.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@typescript-eslint/scope-manager': 6.21.0 + '@typescript-eslint/types': 6.21.0 + '@typescript-eslint/typescript-estree': 6.21.0(typescript@5.1.6) + '@typescript-eslint/visitor-keys': 6.21.0 + debug: 4.3.4(supports-color@8.1.1) + eslint: 8.55.0 + typescript: 5.1.6 + transitivePeerDependencies: + - supports-color + dev: false + /@typescript-eslint/parser@6.7.2(eslint@8.55.0)(typescript@5.1.6): resolution: {integrity: sha512-KA3E4ox0ws+SPyxQf9iSI25R6b4Ne78ORhNHeVKrPQnoYsb9UhieoiRoJgrzgEeKGOXhcY1i8YtOeCHHTDa6Fw==} engines: {node: ^16.0.0 || >=18.0.0} @@ -511,6 +532,14 @@ packages: '@typescript-eslint/visitor-keys': 5.59.11 dev: false + /@typescript-eslint/scope-manager@6.21.0: + resolution: {integrity: sha512-OwLUIWZJry80O99zvqXVEioyniJMa+d2GrqpUTqi5/v5D5rOrppJVBPa0yKCblcigC0/aYAzxxqQ1B+DS2RYsg==} + engines: {node: ^16.0.0 || >=18.0.0} + dependencies: + '@typescript-eslint/types': 6.21.0 + '@typescript-eslint/visitor-keys': 6.21.0 + dev: false + /@typescript-eslint/scope-manager@6.7.2: resolution: {integrity: sha512-bgi6plgyZjEqapr7u2mhxGR6E8WCzKNUFWNh6fkpVe9+yzRZeYtDTbsIBzKbcxI+r1qVWt6VIoMSNZ4r2A+6Yw==} engines: {node: ^16.0.0 || >=18.0.0} @@ -544,6 +573,11 @@ packages: engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dev: false + /@typescript-eslint/types@6.21.0: + resolution: {integrity: sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg==} + engines: {node: ^16.0.0 || >=18.0.0} + dev: false + /@typescript-eslint/types@6.7.2: resolution: {integrity: sha512-flJYwMYgnUNDAN9/GAI3l8+wTmvTYdv64fcH8aoJK76Y+1FCZ08RtI5zDerM/FYT5DMkAc+19E4aLmd5KqdFyg==} engines: {node: ^16.0.0 || >=18.0.0} @@ -570,6 +604,28 @@ packages: - supports-color dev: false + /@typescript-eslint/typescript-estree@6.21.0(typescript@5.1.6): + resolution: {integrity: sha512-6npJTkZcO+y2/kr+z0hc4HwNfrrP4kNYh57ek7yCNlrBjWQ1Y0OS7jiZTkgumrvkX5HkEKXFZkkdFNkaW2wmUQ==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@typescript-eslint/types': 6.21.0 + '@typescript-eslint/visitor-keys': 6.21.0 + debug: 4.3.4(supports-color@8.1.1) + globby: 11.1.0 + is-glob: 4.0.3 + minimatch: 9.0.3 + semver: 7.5.4 + ts-api-utils: 1.0.3(typescript@5.1.6) + typescript: 5.1.6 + transitivePeerDependencies: + - supports-color + dev: false + /@typescript-eslint/typescript-estree@6.7.2(typescript@5.1.6): resolution: {integrity: sha512-kiJKVMLkoSciGyFU0TOY0fRxnp9qq1AzVOHNeN1+B9erKFCJ4Z8WdjAkKQPP+b1pWStGFqezMLltxO+308dJTQ==} engines: {node: ^16.0.0 || >=18.0.0} @@ -638,6 +694,14 @@ packages: eslint-visitor-keys: 3.4.3 dev: false + /@typescript-eslint/visitor-keys@6.21.0: + resolution: {integrity: sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A==} + engines: {node: ^16.0.0 || >=18.0.0} + dependencies: + '@typescript-eslint/types': 6.21.0 + eslint-visitor-keys: 3.4.3 + dev: false + /@typescript-eslint/visitor-keys@6.7.2: resolution: {integrity: sha512-uVw9VIMFBUTz8rIeaUT3fFe8xIUx8r4ywAdlQv1ifH+6acn/XF8Y6rwJ7XNmkNMDrTW+7+vxFFPIF40nJCVsMQ==} engines: {node: ^16.0.0 || >=18.0.0} @@ -928,8 +992,8 @@ packages: wrap-ansi: 7.0.0 dev: true - /code-block-writer@12.0.0: - resolution: {integrity: sha512-q4dMFMlXtKR3XNBHyMHt/3pwYNA69EDk00lloMOaaUMKPUXBw6lpXtbu3MMVG6/uOihGnRDOlkyqsONEUj60+w==} + /code-block-writer@13.0.2: + resolution: {integrity: sha512-XfXzAGiStXSmCIwrkdfvc7FS5Dtj8yelCtyOf2p2skCAfvLd6zu0rGzuS9NSCO3bq1JKpFZ7tbKdKlcd5occQA==} dev: false /color-convert@1.9.3: @@ -1649,6 +1713,17 @@ packages: merge2: 1.4.1 micromatch: 4.0.5 + /fast-glob@3.3.2: + resolution: {integrity: sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==} + engines: {node: '>=8.6.0'} + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.5 + dev: false + /fast-json-stable-stringify@2.1.0: resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} @@ -1832,6 +1907,7 @@ packages: /glob@8.1.0: resolution: {integrity: sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==} engines: {node: '>=12'} + deprecated: Glob versions prior to v9 are no longer supported dependencies: fs.realpath: 1.0.0 inflight: 1.0.6 @@ -2640,9 +2716,16 @@ packages: brace-expansion: 2.0.1 dev: true - /minimatch@7.4.6: - resolution: {integrity: sha512-sBz8G/YjVniEz6lKPNpKxXwazJe4c19fEfV2GDMX6AjFz+MX9uDWIZW8XreVhkFW3fkIdTv/gxWr/Kks5FFAVw==} - engines: {node: '>=10'} + /minimatch@9.0.3: + resolution: {integrity: sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==} + engines: {node: '>=16 || 14 >=14.17'} + dependencies: + brace-expansion: 2.0.1 + dev: false + + /minimatch@9.0.5: + resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} + engines: {node: '>=16 || 14 >=14.17'} dependencies: brace-expansion: 2.0.1 dev: false @@ -2657,8 +2740,8 @@ packages: hasBin: true dev: true - /mkdirp@2.1.6: - resolution: {integrity: sha512-+hEnITedc8LAtIP9u3HJDFIdcLV2vXP33sqLLIzkv1Db1zO/1OxbvYf0Y1OC/S/Qo5dxHXepofhmxL02PsKe+A==} + /mkdirp@3.0.1: + resolution: {integrity: sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==} engines: {node: '>=10'} hasBin: true dev: false @@ -3410,11 +3493,11 @@ packages: typescript: 5.1.6 dev: false - /ts-morph@20.0.0: - resolution: {integrity: sha512-JVmEJy2Wow5n/84I3igthL9sudQ8qzjh/6i4tmYCm6IqYyKFlNbJZi7oBdjyqcWSWYRu3CtL0xbT6fS03ESZIg==} + /ts-morph@22.0.0: + resolution: {integrity: sha512-M9MqFGZREyeb5fTl6gNHKZLqBQA0TjA1lea+CR48R8EBTDuWrNqW6ccC5QvjNR4s6wDumD3LTCjOFSp9iwlzaw==} dependencies: - '@ts-morph/common': 0.21.0 - code-block-writer: 12.0.0 + '@ts-morph/common': 0.23.0 + code-block-writer: 13.0.2 dev: false /tslib@1.14.1: diff --git a/common/build/eslint-config-fluid/printed-configs/default.json b/common/build/eslint-config-fluid/printed-configs/default.json index fb32bf2077e0..c2b1595d0311 100644 --- a/common/build/eslint-config-fluid/printed-configs/default.json +++ b/common/build/eslint-config-fluid/printed-configs/default.json @@ -601,6 +601,11 @@ "error", { "allow": [ + "@fluid-example/*/internal", + "@fluid-experimental/*/internal", + "@fluid-internal/*/internal", + "@fluid-private/*/internal", + "@fluid-tools/*/internal", "@fluidframework/*/internal", "@fluid-experimental/**", "*/index.js" diff --git a/common/build/eslint-config-fluid/printed-configs/minimal.json b/common/build/eslint-config-fluid/printed-configs/minimal.json index d2c756bb6ef4..fd0f45ff1704 100644 --- a/common/build/eslint-config-fluid/printed-configs/minimal.json +++ b/common/build/eslint-config-fluid/printed-configs/minimal.json @@ -591,6 +591,11 @@ "error", { "allow": [ + "@fluid-example/*/internal", + "@fluid-experimental/*/internal", + "@fluid-internal/*/internal", + "@fluid-private/*/internal", + "@fluid-tools/*/internal", "@fluidframework/*/internal", "@fluid-experimental/**", "*/index.js" diff --git a/common/build/eslint-config-fluid/printed-configs/react.json b/common/build/eslint-config-fluid/printed-configs/react.json index 81a0290b72a2..7af7156e8409 100644 --- a/common/build/eslint-config-fluid/printed-configs/react.json +++ b/common/build/eslint-config-fluid/printed-configs/react.json @@ -603,6 +603,11 @@ "error", { "allow": [ + "@fluid-example/*/internal", + "@fluid-experimental/*/internal", + "@fluid-internal/*/internal", + "@fluid-private/*/internal", + "@fluid-tools/*/internal", "@fluidframework/*/internal", "@fluid-experimental/**", "*/index.js" diff --git a/common/build/eslint-config-fluid/printed-configs/recommended.json b/common/build/eslint-config-fluid/printed-configs/recommended.json index fb32bf2077e0..c2b1595d0311 100644 --- a/common/build/eslint-config-fluid/printed-configs/recommended.json +++ b/common/build/eslint-config-fluid/printed-configs/recommended.json @@ -601,6 +601,11 @@ "error", { "allow": [ + "@fluid-example/*/internal", + "@fluid-experimental/*/internal", + "@fluid-internal/*/internal", + "@fluid-private/*/internal", + "@fluid-tools/*/internal", "@fluidframework/*/internal", "@fluid-experimental/**", "*/index.js" diff --git a/common/build/eslint-config-fluid/printed-configs/strict.json b/common/build/eslint-config-fluid/printed-configs/strict.json index da9e11e7087d..53a7b129021b 100644 --- a/common/build/eslint-config-fluid/printed-configs/strict.json +++ b/common/build/eslint-config-fluid/printed-configs/strict.json @@ -620,6 +620,11 @@ "error", { "allow": [ + "@fluid-example/*/internal", + "@fluid-experimental/*/internal", + "@fluid-internal/*/internal", + "@fluid-private/*/internal", + "@fluid-tools/*/internal", "@fluidframework/*/internal", "@fluid-experimental/**", "*/index.js" diff --git a/common/build/eslint-config-fluid/printed-configs/test.json b/common/build/eslint-config-fluid/printed-configs/test.json index d0c3cdf33fc9..201ea7950bb9 100644 --- a/common/build/eslint-config-fluid/printed-configs/test.json +++ b/common/build/eslint-config-fluid/printed-configs/test.json @@ -603,6 +603,11 @@ "allow": [ "@fluid*/*/test*", "@fluid*/*/internal/test*", + "@fluid-example/*/internal", + "@fluid-experimental/*/internal", + "@fluid-internal/*/internal", + "@fluid-private/*/internal", + "@fluid-tools/*/internal", "@fluidframework/*/internal", "@fluid-experimental/**", "*/index.js" diff --git a/common/build/eslint-plugin-fluid/index.js b/common/build/eslint-plugin-fluid/index.js index 60fbccc842c8..4faa22043ef6 100644 --- a/common/build/eslint-plugin-fluid/index.js +++ b/common/build/eslint-plugin-fluid/index.js @@ -13,5 +13,6 @@ module.exports = { */ "no-member-release-tags": require("./src/rules/no-member-release-tags"), "no-restricted-tags-imports": require("./src/rules/no-restricted-tags-imports"), + "no-unchecked-record-access": require("./src/rules/no-unchecked-record-access"), }, }; diff --git a/common/build/eslint-plugin-fluid/package.json b/common/build/eslint-plugin-fluid/package.json index e86c238abaf5..5f5c45c91d6a 100644 --- a/common/build/eslint-plugin-fluid/package.json +++ b/common/build/eslint-plugin-fluid/package.json @@ -1,6 +1,6 @@ { "name": "@fluid-internal/eslint-plugin-fluid", - "version": "0.1.1", + "version": "0.1.2", "description": "Custom ESLint rules for the Fluid Framework", "homepage": "https://fluidframework.com", "repository": { @@ -12,8 +12,9 @@ "author": "Microsoft and contributors", "main": "index.js", "scripts": { - "build": "npm run build:readme", + "build": "npm run build:readme && npm run build:test:examples", "build:readme": "markdown-magic --files \"**/*.md\"", + "build:test:examples": "tsc --project ./src/test/example/tsconfig.json", "clean": "rimraf --glob nyc", "format": "npm run prettier:fix", "prettier": "prettier --check . --cache --ignore-path ../../../.prettierignore", diff --git a/common/build/eslint-plugin-fluid/src/rules/no-unchecked-record-access.js b/common/build/eslint-plugin-fluid/src/rules/no-unchecked-record-access.js new file mode 100644 index 000000000000..83e9d320df6c --- /dev/null +++ b/common/build/eslint-plugin-fluid/src/rules/no-unchecked-record-access.js @@ -0,0 +1,384 @@ +/*! + * Copyright (c) Microsoft Corporation and contributors. All rights reserved. + * Licensed under the MIT License. + */ + +/** + * Rule to enforce safe property access on index signature types. + * + * Reports issues when non-array index properties are accessed without handling + * the possibility that they are absent. + * Enabling `noUncheckedIndexedAccess` will disable these checks. + */ + +const { SyntaxKind, TypeFlags } = require("typescript"); + +module.exports = { + meta: { + type: "problem", + docs: { + description: "Disallow unchecked property access on index signature types", + category: "Possible Errors", + }, + schema: [], + }, + create(context) { + const parserServices = context.parserServices; + + // Check if we have the necessary TypeScript services + if (!parserServices || !parserServices.program || !parserServices.esTreeNodeToTSNodeMap) { + return {}; + } + const compilerOptions = parserServices.program.getCompilerOptions(); + const typeChecker = parserServices.program.getTypeChecker(); + + // If noUncheckedIndexedAccess is already enabled, disable this rule + if (compilerOptions.noUncheckedIndexedAccess) { + return {}; + } + + // Main function to run on every member access (e.g., obj.a or obj["a"]) + function checkPropertyAccess(node) { + // Only check index signature types + if (!isIndexSignatureType(parserServices, node)) { + return; + } + + // Skip if the property has been checked (e.g., with optional chaining). Please see isDefined() for exhaustive list. + if (propertyHasBeenChecked(node)) { + return; + } + + const fullName = getFullName(node); + const parentNode = node.parent; + + /* + * Cases when this lint rule should report a defect + */ + + if (parentNode.type === "VariableDeclarator") { + if ( + parentNode.init === node && + parentNode.parent.type === "VariableDeclaration" && + !parentNode.id.typeAnnotation && + !isUndefinableIndexSignatureType(parserServices, node) + ) { + // This defect occurs when a non-undefinable index signature type is assigned to a implicitly typed variable + return context.report({ + node, + message: `Implicit typing derived from '${fullName}' is not allowed. '${node.object.name}' is an index signature type and '${node.property.name}' may be undefined. Please provide an explicit type annotation including undefined or enable noUncheckedIndexedAccess`, + }); + } + + if ( + parentNode.id.typeAnnotation && + isStrictlyTypedVariable(parentNode.id.typeAnnotation.typeAnnotation) + ) { + // This defect occurs when an index signature type is assigned to a strict variable on variable declaration + return context.report({ + node, + message: `'${fullName}' is possibly 'undefined'`, + }); + } + } + + if (parentNode.type === "AssignmentExpression" && parentNode.right === node) { + if ( + !isUndefinableIndexSignatureType(parserServices, node) && + !isTypeUndefinable(getNodeType(parentNode.left, parserServices)) + ) { + // This defect occurs when a non-undefinable index signature type is assigned to a strictly typed variable + return context.report({ + node, + message: `Assigning '${fullName}' from an index signature type to a strictly typed variable without 'undefined' is not allowed. '${fullName}' may be 'undefined'`, + }); + } + + if (isStrictlyTypedVariable(getVariableType(parentNode.left, context.getScope()))) { + // This defect occurs when an index signature type is assigned to a strictly typed variable after its declaration + return context.report({ + node, + message: `Assigning '${fullName}' from an index signature type to a strictly typed variable without 'undefined' is not allowed. '${fullName}' may be 'undefined'`, + }); + } + } + + if (parentNode.type === "MemberExpression" && parentNode.object === node) { + // This defect occurs when trying to access a property on an index signature type, which might be undefined + return context.report({ + node, + message: `'${fullName}' is possibly 'undefined'`, + }); + } + + if (parentNode.type === "ReturnStatement") { + const functionNode = findParentFunction(node); + if (!functionNode) { + return; + } + const tsNode = parserServices.esTreeNodeToTSNodeMap.get(functionNode); + if (isTypeAllowedToBeUndefined(tsNode, typeChecker)) { + return; + } + // This defect occurs when returning an index signature type from a function that doesn't allow undefined in its return type + return context.report({ + node, + message: `Returning '${fullName}' directly from an index signature type is not allowed. '${fullName}' may be 'undefined'`, + }); + } + + if (parentNode.type === "CallExpression") { + if (parentNode.callee.type !== "Identifier") { + return; + } + const functionDeclaration = findFunctionDeclaration( + parentNode.callee.name, + context.getScope(), + ); + if (!functionDeclaration || !functionDeclaration.params) { + return; + } + const paramIndex = parentNode.arguments.indexOf(node); + if (paramIndex === -1 || paramIndex >= functionDeclaration.params.length) { + return; + } + const paramType = getFunctionParameterType(functionDeclaration.params[paramIndex]); + if (!paramType || !isStrictlyTypedParameter(paramType)) { + return; + } + // This defect occurs when passing an index signature type to a function parameter that doesn't allow undefined + return context.report({ + node, + message: `Passing '${fullName}' from an index signature type to a strictly typed parameter is not allowed. '${fullName}' may be 'undefined'`, + }); + } + } + + return { + MemberExpression: checkPropertyAccess, + }; + }, +}; + +// Helper function to check if a type has an index signature +function isIndexSignatureType(parserServices, node) { + const tsNode = parserServices.esTreeNodeToTSNodeMap.get(node.object); + const typeChecker = parserServices.program.getTypeChecker(); + const type = typeChecker.getTypeAtLocation(tsNode); + return type.getStringIndexType() !== undefined; +} + +// Helper function to check if a type includes undefined +function isTypeUndefinable(type) { + if (type.isUnion()) { + return type.types.some((t) => t.flags & TypeFlags.Undefined); + } + return false; +} + +// Helper function to check if an index signature type includes undefined +function isUndefinableIndexSignatureType(parserServices, node) { + const tsNode = parserServices.esTreeNodeToTSNodeMap.get(node.object); + const typeChecker = parserServices.program.getTypeChecker(); + const type = typeChecker.getTypeAtLocation(tsNode); + const indexType = type.getStringIndexType(); + return ( + indexType && + (indexType.flags & TypeFlags.Undefined || + (indexType.isUnion() && indexType.types.some((t) => t.flags & TypeFlags.Undefined))) + ); +} + +// Helper function to traverse up the code until the scope ends and checks if the property access has been checked for undefined +function propertyHasBeenChecked(node) { + let current = node; + while (current) { + if (isDefined(current)) { + return true; + } + current = current.parent; + } + return false; +} + +// Helper function to get the type of a node +function getNodeType(node, parserServices) { + const tsNode = parserServices.esTreeNodeToTSNodeMap.get(node); + const type = parserServices.program.getTypeChecker().getTypeAtLocation(tsNode); + return type; +} + +// Helper function to determine if a node is defined. This has all the cases which define +function isDefined(node) { + if (!node.parent) { + return false; + } + + // Optional chaining or non-null assertion + if (node.optional === true || node.parent.type === "TSNonNullExpression") { + return true; + } + + // Presence check in if statement + if (node.parent.type === "IfStatement" && node.parent.test === node) { + return true; + } + + // 'in' operator check + if ( + node.parent.type === "BinaryExpression" && + node.parent.operator === "in" && + node.parent.left === node + ) { + return true; + } + + // Object.entries() or Object.keys() loop + if ( + node.parent.type === "ForOfStatement" && + node.parent.right && + node.parent.right.callee && + node.parent.right.callee.property && + (node.parent.right.callee.property.name === "entries" || + node.parent.right.callee.property.name === "keys") + ) { + return true; + } + + // Check for block statements in if or for...of loops + if (node.parent.type === "BlockStatement") { + const blockParent = node.parent.parent; + if ( + blockParent && + (blockParent.type === "IfStatement" || blockParent.type === "ForOfStatement") + ) { + return isDefined(blockParent.test || blockParent.right); + } + } + + return false; +} + +// Helper function to get the full name of a property access chain +function getFullName(node) { + let fullPath = ""; + let currentNode = node; + + while (currentNode && currentNode.type === "MemberExpression") { + const propertyPart = currentNode.computed + ? `[${currentNode.property.name || currentNode.property.raw}]` + : `.${currentNode.property.name}`; + + fullPath = propertyPart + fullPath; + + if (currentNode.object && currentNode.object.type === "Identifier") { + fullPath = currentNode.object.name + fullPath; + } + + currentNode = currentNode.object; + } + return fullPath; +} + +// Helper function to find the parent function of a node +function findParentFunction(node) { + while (node) { + if ( + node.type === "FunctionDeclaration" || + node.type === "FunctionExpression" || + node.type === "ArrowFunctionExpression" + ) { + return node; + } + node = node.parent; + } + return null; +} + +// Helper function to check if a type is allowed to be undefined (e.g., Promise) +function isTypeAllowedToBeUndefined(tsNode, typeChecker) { + const type = typeChecker.getTypeAtLocation(tsNode); + const symbol = type.getSymbol(); + + if (!symbol || !symbol.valueDeclaration) { + return false; + } + const signatureDeclaration = symbol.valueDeclaration; + // Check for Promise + if (signatureDeclaration.type && signatureDeclaration.type.kind === SyntaxKind.TypeReference) { + const typeNode = signatureDeclaration.type; + if (typeNode.typeName.text === "Promise") { + return ( + typeNode.typeArguments && + typeNode.typeArguments.some( + (arg) => + arg.kind === SyntaxKind.UnionType && + arg.types.some((t) => t.kind === SyntaxKind.UndefinedKeyword), + ) + ); + } + } + // Check for direct union with undefined + return ( + signatureDeclaration.type && + signatureDeclaration.type.kind === SyntaxKind.UnionType && + signatureDeclaration.type.types.some((t) => t.kind === SyntaxKind.UndefinedKeyword) + ); +} + +// Helper function to find a function declaration in the current scope +function findFunctionDeclaration(name, scope) { + const variable = scope.set.get(name); + if (variable && variable.defs.length > 0) { + return variable.defs[0].node; + } + return null; +} + +// Helper function to get the type of a function parameter +function getFunctionParameterType(param) { + if (!param || !param.typeAnnotation || !param.typeAnnotation.typeAnnotation) { + return null; + } + return param.typeAnnotation.typeAnnotation; +} + +// Helper function to check if a parameter is strictly typed (doesn't allow undefined) +function isStrictlyTypedParameter(typeAnnotation) { + if (!typeAnnotation) { + return false; + } + + if (typeAnnotation.type === "TSUnionType") { + return !typeAnnotation.types.some((t) => t.type === "TSUndefinedKeyword"); + } + + // Consider any non-union type as strictly typed + return true; +} + +// Helper function to get the type of a variable from its declaration +function getVariableType(node, scope) { + if (node.type === "Identifier") { + const variable = scope.variables.find((v) => v.name === node.name); + if (variable && variable.defs.length > 0) { + const def = variable.defs[0]; + if (def.node.type === "VariableDeclarator" && def.node.id.typeAnnotation) { + return def.node.id.typeAnnotation.typeAnnotation; + } + } + } + return null; +} + +// Helper function to check if a variable is strictly typed (doesn't allow undefined) +function isStrictlyTypedVariable(typeAnnotation) { + if (!typeAnnotation) return false; + + if (typeAnnotation.type === "TSUnionType") { + return !typeAnnotation.types.some((t) => t.type === "TSUndefinedKeyword"); + } + + // Consider any non-union type as strictly typed, except for 'any' and 'unknown' + return typeAnnotation.type !== "TSAnyKeyword" && typeAnnotation.type !== "TSUnknownKeyword"; +} diff --git a/common/build/eslint-plugin-fluid/src/test/enforce-no-member-release-tags/enforce-no-member-release-tags.test.js b/common/build/eslint-plugin-fluid/src/test/enforce-no-member-release-tags/enforce-no-member-release-tags.test.js index c0d6a9d2c4ce..193e78e390c3 100644 --- a/common/build/eslint-plugin-fluid/src/test/enforce-no-member-release-tags/enforce-no-member-release-tags.test.js +++ b/common/build/eslint-plugin-fluid/src/test/enforce-no-member-release-tags/enforce-no-member-release-tags.test.js @@ -15,7 +15,7 @@ describe("Do not allow release tags on members", function () { }, parser: "@typescript-eslint/parser", parserOptions: { - project: path.join(__dirname, "../tsconfig.json"), + project: path.join(__dirname, "../example/tsconfig.json"), }, }; @@ -30,7 +30,7 @@ describe("Do not allow release tags on members", function () { const eslint = createESLintInstance(); const filesToLint = ["mockClassDeclaration.ts"].map((file) => - path.join(__dirname, "../mockFiles/no-member-release-tags", file), + path.join(__dirname, "../example/no-member-release-tags", file), ); const results = await eslint.lintFiles(filesToLint); @@ -75,7 +75,7 @@ describe("Do not allow release tags on members", function () { const eslint = createESLintInstance(); const filesToLint = ["mockClassExpression.ts"].map((file) => - path.join(__dirname, "../mockFiles/no-member-release-tags", file), + path.join(__dirname, "../example/no-member-release-tags", file), ); const results = await eslint.lintFiles(filesToLint); const result = results[0]; @@ -126,7 +126,7 @@ describe("Do not allow release tags on members", function () { const eslint = createESLintInstance(); const filesToLint = ["mockAbstractClass.ts"].map((file) => - path.join(__dirname, "../mockFiles/no-member-release-tags", file), + path.join(__dirname, "../example/no-member-release-tags", file), ); const results = await eslint.lintFiles(filesToLint); const result = results[0]; @@ -145,7 +145,7 @@ describe("Do not allow release tags on members", function () { const eslint = createESLintInstance(); const filesToLint = ["mockInterface.ts"].map((file) => - path.join(__dirname, "../mockFiles/no-member-release-tags", file), + path.join(__dirname, "../example/no-member-release-tags", file), ); const results = await eslint.lintFiles(filesToLint); const result = results[0]; @@ -184,7 +184,7 @@ describe("Do not allow release tags on members", function () { const eslint = createESLintInstance(); const filesToLint = ["mockType.ts"].map((file) => - path.join(__dirname, "../mockFiles/no-member-release-tags", file), + path.join(__dirname, "../example/no-member-release-tags", file), ); const results = await eslint.lintFiles(filesToLint); const result = results[0]; @@ -220,7 +220,7 @@ describe("Do not allow release tags on members", function () { const eslint = createESLintInstance(); const filesToLint = ["mockFunction.ts"].map((file) => - path.join(__dirname, "../mockFiles/no-member-release-tags", file), + path.join(__dirname, "../example/no-member-release-tags", file), ); const results = await eslint.lintFiles(filesToLint); const result = results[0]; diff --git a/common/build/eslint-plugin-fluid/src/test/enforce-no-restricted-tags-imports/enforce-no-restricted-tags-imports.test.js b/common/build/eslint-plugin-fluid/src/test/enforce-no-restricted-tags-imports/enforce-no-restricted-tags-imports.test.js index e65a20c94f01..da4b17346c89 100644 --- a/common/build/eslint-plugin-fluid/src/test/enforce-no-restricted-tags-imports/enforce-no-restricted-tags-imports.test.js +++ b/common/build/eslint-plugin-fluid/src/test/enforce-no-restricted-tags-imports/enforce-no-restricted-tags-imports.test.js @@ -29,11 +29,11 @@ describe("ESLint Rule Tests", function () { }, parser: "@typescript-eslint/parser", parserOptions: { - project: path.join(__dirname, "../tsconfig.json"), + project: path.join(__dirname, "../example/tsconfig.json"), }, }); const filesToLint = ["fileWithImports.ts", "mockModule.ts"].map((file) => - path.join(__dirname, "../mockFiles/no-restricted-tags-imports", file), + path.join(__dirname, "../example/no-restricted-tags-imports", file), ); const results = await eslint.lintFiles(filesToLint); const result = results[0]; @@ -64,11 +64,11 @@ describe("ESLint Rule Tests", function () { }, parser: "@typescript-eslint/parser", parserOptions: { - project: path.join(__dirname, "../tsconfig.json"), + project: path.join(__dirname, "../example/tsconfig.json"), }, }); const filesToLint = ["fileWithExceptionImports.ts", "exceptionFile.ts"].map((file) => - path.join(__dirname, "../mockFiles/no-restricted-tags-imports", file), + path.join(__dirname, "../example/no-restricted-tags-imports", file), ); const results = await eslint.lintFiles(filesToLint); const result = results[0]; @@ -92,11 +92,11 @@ describe("ESLint Rule Tests", function () { }, parser: "@typescript-eslint/parser", parserOptions: { - project: path.join(__dirname, "../tsconfig.json"), + project: path.join(__dirname, "../example/tsconfig.json"), }, }); const filesToLint = ["fileWithImports.ts", "mockModule.ts"].map((file) => - path.join(__dirname, "../mockFiles/no-restricted-tags-imports", file), + path.join(__dirname, "../example/no-restricted-tags-imports", file), ); const results = await eslint.lintFiles(filesToLint); const result = results[0]; diff --git a/common/build/eslint-plugin-fluid/src/test/enforce-no-unchecked-record-access/enforce-no-unchecked-record-access.test.js b/common/build/eslint-plugin-fluid/src/test/enforce-no-unchecked-record-access/enforce-no-unchecked-record-access.test.js new file mode 100644 index 000000000000..0571fb809403 --- /dev/null +++ b/common/build/eslint-plugin-fluid/src/test/enforce-no-unchecked-record-access/enforce-no-unchecked-record-access.test.js @@ -0,0 +1,171 @@ +/*! + * Copyright (c) Microsoft Corporation and contributors. All rights reserved. + * Licensed under the MIT License. + */ + +const assert = require("assert"); +const path = require("path"); +const { ESLint } = require("eslint"); + +describe("ESLint Rule Tests", function () { + async function lintFile(file) { + const eslint = new ESLint({ + useEslintrc: false, + overrideConfig: { + rules: { + "no-unchecked-record-access": "error", + }, + parser: "@typescript-eslint/parser", + parserOptions: { + project: path.join(__dirname, "../example/tsconfig.json"), + }, + }, + rulePaths: [path.join(__dirname, "../../rules")], + }); + const fileToLint = path.join(__dirname, "../example/no-unchecked-record-access", file); + const results = await eslint.lintFiles([fileToLint]); + return results[0]; + } + + it("Should report an error for unchecked record access for indexed record of strings in generics", async function () { + const result = await lintFile("generics.ts"); + assert.strictEqual(result.errorCount, 1, "Should have 1 error"); + assert.strictEqual( + result.messages[0].message, + "Returning 'record.a' directly from an index signature type is not allowed. 'record.a' may be 'undefined'", + ); + assert.strictEqual(result.messages[0].line, 11); + }); + + it("Should report errors for unchecked record access for indexed record of strings", async function () { + const result = await lintFile("indexedRecordOfStrings.ts"); + + assert.strictEqual(result.errorCount, 10, "Should have 10 errors"); + + assert.strictEqual( + result.messages[0].message, + "'indexedRecordOfStrings.a' is possibly 'undefined'", + ); + assert.strictEqual(result.messages[0].line, 21); + + assert.strictEqual( + result.messages[1].message, + "'indexedRecordOfStrings[\"a\"]' is possibly 'undefined'", + ); + assert.strictEqual(result.messages[1].line, 23); + + assert.strictEqual( + result.messages[2].message, + "'indexedRecordOfStrings[a]' is possibly 'undefined'", + ); + assert.strictEqual(result.messages[2].line, 24); + + assert.strictEqual( + result.messages[3].message, + "'indexedRecordOfStrings[b]' is possibly 'undefined'", + ); + assert.strictEqual(result.messages[3].line, 25); + + assert.strictEqual( + result.messages[4].message, + "Returning 'record.a' directly from an index signature type is not allowed. 'record.a' may be 'undefined'", + ); + assert.strictEqual(result.messages[4].line, 42); + + assert.strictEqual( + result.messages[5].message, + "Passing 'indexedRecordOfStrings.a' from an index signature type to a strictly typed parameter is not allowed. 'indexedRecordOfStrings.a' may be 'undefined'", + ); + assert.strictEqual(result.messages[5].line, 57); + + assert.strictEqual( + result.messages[6].message, + "'indexedRecordOfStrings.a' is possibly 'undefined'", + ); + assert.strictEqual(result.messages[6].line, 76); + + assert.strictEqual( + result.messages[7].message, + "'indexedRecordOfStrings.a' is possibly 'undefined'", + ); + assert.strictEqual(result.messages[7].line, 78); + + assert.strictEqual( + result.messages[8].message, + "Assigning 'indexedRecordOfStrings.a' from an index signature type to a strictly typed variable without 'undefined' is not allowed. 'indexedRecordOfStrings.a' may be 'undefined'", + ); + assert.strictEqual(result.messages[8].line, 81); + + assert.strictEqual( + result.messages[9].message, + "Implicit typing derived from 'indexedRecordOfStrings.a' is not allowed. 'indexedRecordOfStrings' is an index signature type and 'a' may be undefined. Please provide an explicit type annotation including undefined or enable noUncheckedIndexedAccess", + ); + assert.strictEqual(result.messages[9].line, 88); + }); + + it("Should report an error for unchecked nested record access", async function () { + const result = await lintFile("nestedIndexSignatures.ts"); + assert.strictEqual(result.errorCount, 1, "Should have 1 error"); + assert.strictEqual( + result.messages[0].message, + "'nestedObj.nested.a' is possibly 'undefined'", + ); + assert.strictEqual(result.messages[0].line, 18); + }); + + it("Should report errors for unchecked record access in nullableIndexedRecord", async function () { + const result = await lintFile("nullableIndexedRecord.ts"); + assert.strictEqual(result.errorCount, 6, "Should have 6 errors"); + + assert.strictEqual( + result.messages[0].message, + "Returning 'record.a' directly from an index signature type is not allowed. 'record.a' may be 'undefined'", + ); + assert.strictEqual(result.messages[0].line, 21); + + assert.strictEqual( + result.messages[1].message, + "Passing 'nullableIndexedRecord.a' from an index signature type to a strictly typed parameter is not allowed. 'nullableIndexedRecord.a' may be 'undefined'", + ); + assert.strictEqual(result.messages[1].line, 40); + + assert.strictEqual( + result.messages[2].message, + "'nullableIndexedRecord.a' is possibly 'undefined'", + ); + assert.strictEqual(result.messages[2].line, 47); + + assert.strictEqual( + result.messages[3].message, + "'nullableIndexedRecord.a' is possibly 'undefined'", + ); + assert.strictEqual(result.messages[3].line, 48); + + assert.strictEqual( + result.messages[4].message, + "Assigning 'nullableIndexedRecord.a' from an index signature type to a strictly typed variable without 'undefined' is not allowed. 'nullableIndexedRecord.a' may be 'undefined'", + ); + assert.strictEqual(result.messages[4].line, 53); + + assert.strictEqual( + result.messages[5].message, + "Implicit typing derived from 'nullableIndexedRecord.a' is not allowed. 'nullableIndexedRecord' is an index signature type and 'a' may be undefined. Please provide an explicit type annotation including undefined or enable noUncheckedIndexedAccess", + ); + assert.strictEqual(result.messages[5].line, 60); + }); + + it("Should not report errors for correct usage of undefinableIndexedRecord", async function () { + const result = await lintFile("undefinableIndexedRecord.ts"); + assert.strictEqual(result.errorCount, 0, "Should have no errors"); + }); + + it("Should not report errors for static types", async function () { + const result = await lintFile("staticTypes.ts"); + assert.strictEqual(result.errorCount, 0, "Should have no errors"); + }); + + it("Should not report an error for valid array access", async function () { + const result = await lintFile("fileWithOnlyArrayAccess.ts"); + assert.strictEqual(result.errorCount, 0, "Should have no errors"); + }); +}); diff --git a/common/build/eslint-plugin-fluid/src/test/mockFiles/no-member-release-tags/mockAbstractClass.ts b/common/build/eslint-plugin-fluid/src/test/example/no-member-release-tags/mockAbstractClass.ts similarity index 100% rename from common/build/eslint-plugin-fluid/src/test/mockFiles/no-member-release-tags/mockAbstractClass.ts rename to common/build/eslint-plugin-fluid/src/test/example/no-member-release-tags/mockAbstractClass.ts diff --git a/common/build/eslint-plugin-fluid/src/test/mockFiles/no-member-release-tags/mockClassDeclaration.ts b/common/build/eslint-plugin-fluid/src/test/example/no-member-release-tags/mockClassDeclaration.ts similarity index 93% rename from common/build/eslint-plugin-fluid/src/test/mockFiles/no-member-release-tags/mockClassDeclaration.ts rename to common/build/eslint-plugin-fluid/src/test/example/no-member-release-tags/mockClassDeclaration.ts index bbe33ee9f4f1..79386a0aa588 100644 --- a/common/build/eslint-plugin-fluid/src/test/mockFiles/no-member-release-tags/mockClassDeclaration.ts +++ b/common/build/eslint-plugin-fluid/src/test/example/no-member-release-tags/mockClassDeclaration.ts @@ -40,7 +40,7 @@ class MockClass { validNoComment(): void {} - signature: string; + signature: string = "signature"; private _value = 1; @@ -69,5 +69,5 @@ class MockClassTwo { invalidInternalTwo(): void {} // Valid property signature. - validSignature: void; + validSignature: null = null; } diff --git a/common/build/eslint-plugin-fluid/src/test/mockFiles/no-member-release-tags/mockClassExpression.ts b/common/build/eslint-plugin-fluid/src/test/example/no-member-release-tags/mockClassExpression.ts similarity index 88% rename from common/build/eslint-plugin-fluid/src/test/mockFiles/no-member-release-tags/mockClassExpression.ts rename to common/build/eslint-plugin-fluid/src/test/example/no-member-release-tags/mockClassExpression.ts index 40abd4813d3c..54572f0edb82 100644 --- a/common/build/eslint-plugin-fluid/src/test/mockFiles/no-member-release-tags/mockClassExpression.ts +++ b/common/build/eslint-plugin-fluid/src/test/example/no-member-release-tags/mockClassExpression.ts @@ -31,12 +31,12 @@ const mockClassExpression = class { invalidLineComment(): void {} // @alpha - invalidLineSignature: string; + invalidLineSignature: string = "invalidLineSignature"; /** * @internal */ - inValidSingature: string; + inValidSingature: string = "inValidSingature"; /** * Correctly implemented method with valid comment. @@ -48,7 +48,7 @@ const mockClassExpression = class { validNoComment(): void {} - validSignature: boolean; + validSignature: boolean = false; /** * @public diff --git a/common/build/eslint-plugin-fluid/src/test/mockFiles/no-member-release-tags/mockFunction.ts b/common/build/eslint-plugin-fluid/src/test/example/no-member-release-tags/mockFunction.ts similarity index 100% rename from common/build/eslint-plugin-fluid/src/test/mockFiles/no-member-release-tags/mockFunction.ts rename to common/build/eslint-plugin-fluid/src/test/example/no-member-release-tags/mockFunction.ts diff --git a/common/build/eslint-plugin-fluid/src/test/mockFiles/no-member-release-tags/mockInterface.ts b/common/build/eslint-plugin-fluid/src/test/example/no-member-release-tags/mockInterface.ts similarity index 100% rename from common/build/eslint-plugin-fluid/src/test/mockFiles/no-member-release-tags/mockInterface.ts rename to common/build/eslint-plugin-fluid/src/test/example/no-member-release-tags/mockInterface.ts diff --git a/common/build/eslint-plugin-fluid/src/test/mockFiles/no-member-release-tags/mockType.ts b/common/build/eslint-plugin-fluid/src/test/example/no-member-release-tags/mockType.ts similarity index 100% rename from common/build/eslint-plugin-fluid/src/test/mockFiles/no-member-release-tags/mockType.ts rename to common/build/eslint-plugin-fluid/src/test/example/no-member-release-tags/mockType.ts diff --git a/common/build/eslint-plugin-fluid/src/test/mockFiles/no-restricted-tags-imports/exceptionFile.ts b/common/build/eslint-plugin-fluid/src/test/example/no-restricted-tags-imports/exceptionFile.ts similarity index 100% rename from common/build/eslint-plugin-fluid/src/test/mockFiles/no-restricted-tags-imports/exceptionFile.ts rename to common/build/eslint-plugin-fluid/src/test/example/no-restricted-tags-imports/exceptionFile.ts diff --git a/common/build/eslint-plugin-fluid/src/test/mockFiles/no-restricted-tags-imports/fileWithExceptionImports.ts b/common/build/eslint-plugin-fluid/src/test/example/no-restricted-tags-imports/fileWithExceptionImports.ts similarity index 100% rename from common/build/eslint-plugin-fluid/src/test/mockFiles/no-restricted-tags-imports/fileWithExceptionImports.ts rename to common/build/eslint-plugin-fluid/src/test/example/no-restricted-tags-imports/fileWithExceptionImports.ts diff --git a/common/build/eslint-plugin-fluid/src/test/mockFiles/no-restricted-tags-imports/fileWithImports.ts b/common/build/eslint-plugin-fluid/src/test/example/no-restricted-tags-imports/fileWithImports.ts similarity index 100% rename from common/build/eslint-plugin-fluid/src/test/mockFiles/no-restricted-tags-imports/fileWithImports.ts rename to common/build/eslint-plugin-fluid/src/test/example/no-restricted-tags-imports/fileWithImports.ts diff --git a/common/build/eslint-plugin-fluid/src/test/mockFiles/no-restricted-tags-imports/mockModule.ts b/common/build/eslint-plugin-fluid/src/test/example/no-restricted-tags-imports/mockModule.ts similarity index 100% rename from common/build/eslint-plugin-fluid/src/test/mockFiles/no-restricted-tags-imports/mockModule.ts rename to common/build/eslint-plugin-fluid/src/test/example/no-restricted-tags-imports/mockModule.ts diff --git a/common/build/eslint-plugin-fluid/src/test/example/no-unchecked-record-access/fileWithOnlyArrayAccess.ts b/common/build/eslint-plugin-fluid/src/test/example/no-unchecked-record-access/fileWithOnlyArrayAccess.ts new file mode 100644 index 000000000000..fc063f47464c --- /dev/null +++ b/common/build/eslint-plugin-fluid/src/test/example/no-unchecked-record-access/fileWithOnlyArrayAccess.ts @@ -0,0 +1,7 @@ +/*! + * Copyright (c) Microsoft Corporation and contributors. All rights reserved. + * Licensed under the MIT License. + */ + +const arr2 = [1, 2, 3]; +const value2 = arr2[0]; // This should not report an error diff --git a/common/build/eslint-plugin-fluid/src/test/example/no-unchecked-record-access/generics.ts b/common/build/eslint-plugin-fluid/src/test/example/no-unchecked-record-access/generics.ts new file mode 100644 index 000000000000..fbca74e66fd2 --- /dev/null +++ b/common/build/eslint-plugin-fluid/src/test/example/no-unchecked-record-access/generics.ts @@ -0,0 +1,16 @@ +/*! + * Copyright (c) Microsoft Corporation and contributors. All rights reserved. + * Licensed under the MIT License. + */ + +/* + * Generics + */ + +function readRecordA(record: Record): T { + return record.a; // defect: Returning 'record.a' directly from an index signature type is not allowed. It may be 'undefined' +} + +function readOptionalRecordA(record: Record): T | undefined { + return record.a; // ok: Returning index property 'a' to string or undefined variable, 'a' might not be present +} diff --git a/common/build/eslint-plugin-fluid/src/test/example/no-unchecked-record-access/indexedRecordOfStrings.ts b/common/build/eslint-plugin-fluid/src/test/example/no-unchecked-record-access/indexedRecordOfStrings.ts new file mode 100644 index 000000000000..1c7111a524e3 --- /dev/null +++ b/common/build/eslint-plugin-fluid/src/test/example/no-unchecked-record-access/indexedRecordOfStrings.ts @@ -0,0 +1,89 @@ +/*! + * Copyright (c) Microsoft Corporation and contributors. All rights reserved. + * Licensed under the MIT License. + */ + +/* + * Index signature type + */ + +/* Constants and Variables */ +type IndexSignatureType = { [key: string]: string }; +const indexedRecordOfStrings: IndexSignatureType = { a: "hello", b: "goodbye" }; +const a = "a"; +const b = "b"; + +/* + * Accessing Properties + */ + +indexedRecordOfStrings.a; // ok: Accessing index property 'a' without requiring a particular result +indexedRecordOfStrings.a.length; // defect: Accessing length of index property 'a', but 'a' might not be present +indexedRecordOfStrings["a"]; // ok: Accessing property 'a' using bracket notation +indexedRecordOfStrings["a"].length; // defect: Accessing length of index property 'a' using bracket notation, but 'a' might not be present +indexedRecordOfStrings[a].length; // defect: Accessing length of index property 'a' using bracket notation, but 'a' might not be present +indexedRecordOfStrings[b].length; // defect: Accessing length of index property 'b' using bracket notation, but 'b' might not be present +indexedRecordOfStrings.a?.length; // ok: Using optional chaining to access length safely handles 'undefined' +indexedRecordOfStrings.a!.length; // ok: The author says they understand the question raised by check and acknowledge that they have other information expecting that it is actually defined or that they are okay with an exception being raise here if "a" is not present and defined +indexedRecordOfStrings["a"]?.length; // ok: Using optional chaining to access length using bracket notation safely handles 'undefined' +indexedRecordOfStrings["a"]!.length; // ok: The author says they understand the question raised by check and acknowledge that they have other information expecting that it is actually defined or that they are okay with an exception being raise here if "a" is not present and defined + +/* Conditional Checks */ +if (indexedRecordOfStrings.a) { + indexedRecordOfStrings.a.length; // ok: Within a presence check, 'a' is guaranteed to be defined +} + +if ("a" in indexedRecordOfStrings) { + indexedRecordOfStrings.a.length; // ok: Accessing length of property inside an 'in' check, 'a' is guaranteed to be defined +} + +/* Function Calls */ +function recordAFnExpectsString(record: IndexSignatureType): string { + return record.a; // defect: Returning index property 'a' directly, but 'a' might not be present +} + +function recordAFnExpectsStringOrUndefined(record: IndexSignatureType): string | undefined { + return record.a; // ok: Returning index property 'a' to string or undefined variable, 'a' might not be present +} + +function AFnExpectsString(a: string): string { + return a; +} + +function AFnExpectsStringOrUndefined(a: string | undefined): string | undefined { + return a; +} + +AFnExpectsString(indexedRecordOfStrings.a); // defect: Passing index property 'a' to a function that expects a string should fail +AFnExpectsStringOrUndefined(indexedRecordOfStrings.a); // ok: Passing index property 'a' to a function that accepts undefined is fine + +/* Looping */ +for (const [key, value] of Object.entries(indexedRecordOfStrings)) { + value.length; // ok: Object.entries provides only present values + indexedRecordOfStrings[key]; // ok: Accessing property while looping though records which acts like a `has` property check + indexedRecordOfStrings[key].length; // ok: When noUncheckedIndexedAccess is enabled, TSC will treat indexedRecordOfStrings[key] as an error, but no-unchecked-record-access does not because accessing properties while looping though records acts like a presence check +} + +for (const key of Object.keys(indexedRecordOfStrings)) { + indexedRecordOfStrings[key]; // ok: Accessing property while looping though records which acts like a `has` property check + indexedRecordOfStrings[key].length; // ok: When noUncheckedIndexedAccess is enabled, TSC will treat indexedRecordOfStrings[key] as an error, but no-unchecked-record-access does not because accessing properties while looping though records acts like a presence check +} + +/* + * Variable Assignments + */ + +const aExpectingString: string = indexedRecordOfStrings.a; // defect: Assigning index property 'a' to a strict string variable, but 'a' might not be present +const aExpectingStringOrUndefined: string | undefined = indexedRecordOfStrings.a; // ok: Assigning index property 'a' to string or undefined variable, 'a' might not be present +let aLetExpectingString: string = indexedRecordOfStrings.a; // defect: Assigning index property 'a' to a strict string variable, but 'a' might not be present +let aLetExpectingStringOrUndefined: string | undefined = indexedRecordOfStrings.a; // ok: Assigning index property 'a' to string or undefined variable, 'a' might not be present +let aLetExpectingStringAfterVariableDeclaration: string; +aLetExpectingStringAfterVariableDeclaration = indexedRecordOfStrings.a; // defect: Assigning index property 'a' to a strict string variable, but 'a' might not be present +let aLetExpectingStringOrUndefinedAfterVariableDeclaration: string | undefined; +aLetExpectingStringOrUndefinedAfterVariableDeclaration = indexedRecordOfStrings.a; // ok: Assigning index property 'a' to string or undefined variable, 'a' might not be present + +/* + * When noUncheckedIndexedAccess is enabled, TSC will treat property access on aImplicitType as an error, but no-unchecked-record-access causes an error if an index signature is not typed to allow undefined. + */ +const aImplicitType = indexedRecordOfStrings.a; // defect: Assigning index property with inferred type without an explicit undefined type is not allowed +aImplicitType.length; // ok: aImplicitType is the continuation of the inferred type case and should be caught in the variable initialization diff --git a/common/build/eslint-plugin-fluid/src/test/example/no-unchecked-record-access/nestedIndexSignatures.ts b/common/build/eslint-plugin-fluid/src/test/example/no-unchecked-record-access/nestedIndexSignatures.ts new file mode 100644 index 000000000000..2601cf84eebe --- /dev/null +++ b/common/build/eslint-plugin-fluid/src/test/example/no-unchecked-record-access/nestedIndexSignatures.ts @@ -0,0 +1,18 @@ +/*! + * Copyright (c) Microsoft Corporation and contributors. All rights reserved. + * Licensed under the MIT License. + */ + +/* + * Nested index signature + */ + +interface NestedIndexProps { + nested: { + [key: string]: string; + }; +} +/* Nested Index Signatures */ +const nestedObj: NestedIndexProps = { nested: { a: "hello" } }; +nestedObj.nested.a; // ok: Accessing nested index property 'a' without requiring a particular result +nestedObj.nested.a.length; // defect: Accessing length of a nested possibly undefined or missing property diff --git a/common/build/eslint-plugin-fluid/src/test/example/no-unchecked-record-access/nullableIndexedRecord.ts b/common/build/eslint-plugin-fluid/src/test/example/no-unchecked-record-access/nullableIndexedRecord.ts new file mode 100644 index 000000000000..61212a27bd2b --- /dev/null +++ b/common/build/eslint-plugin-fluid/src/test/example/no-unchecked-record-access/nullableIndexedRecord.ts @@ -0,0 +1,61 @@ +/*! + * Copyright (c) Microsoft Corporation and contributors. All rights reserved. + * Licensed under the MIT License. + */ + +/* + * Nullable index signature + */ + +/* Constants and Variables */ +type NullableIndexSignatureType = { [key: string]: string | null }; +const nullableIndexedRecord: NullableIndexSignatureType = { a: "hello", b: null }; + +/* Conditional Checks */ +if (nullableIndexedRecord.a) { + nullableIndexedRecord.a.length; // ok: Within a presence check, 'a' is guaranteed to be defined +} + +/* Function Calls */ +function recordAFnExpectsStringOrNull(record: NullableIndexSignatureType): string | null { + return record.a; // defect: Returning index property 'a' as string or null variable should be caught, 'a' might be undefined +} + +function recordAFnExpectsStringOrUndefinedOrNull( + record: NullableIndexSignatureType, +): string | undefined | null { + return record.a; // ok: Returning index property 'a' to string or undefined or null variable, 'a' might be undefined +} + +function AFnExpectsStringOrNull(a: string | null): string | null { + return a; +} + +function AFnExpectsStringOrUndefinedOrNull( + a: string | undefined | null, +): string | undefined | null { + return a; +} + +AFnExpectsStringOrNull(nullableIndexedRecord.a); // defect: Passing index property 'a' to a function without having type undefined +AFnExpectsStringOrUndefinedOrNull(nullableIndexedRecord.a); // ok: Passing index property 'a' to a function that accepts undefined is fine + +/* + * Variable Assignments + */ + +const aExpectingStringOrNull: string | null = nullableIndexedRecord.a; // defect: Assigning index property 'a' to string or null variable, 'a' might not be present, type should include an undefined type +let aLetExpectingStringOrNull: string | null = nullableIndexedRecord.a; // defect: Assigning index property 'a' to string or null variable, 'a' might not be present, either the index signature type should include an undefined type or the variable declaration should be changed to string | null | undefined +const aExpectingStringOrNullOrUndefined: string | null | undefined = nullableIndexedRecord.a; // ok: Assigning index property 'a' to string or null or undefined variable is fine, 'a' might not be present +let aLetExpectingStringOrNullOrUndefined: string | null | undefined = nullableIndexedRecord.a; // ok: Assigning index property 'a' to string or null or undefined variable is fine, 'a' might not be present + +let aLetExpectingStringOrNullAfterVariableDeclaration: string | null; +aLetExpectingStringOrNullAfterVariableDeclaration = nullableIndexedRecord.a; // defect: Assigning index property 'a' to string or null variable should report an error, 'a' might not be present, either the index signature type should include an undefined type or the variable declaration should be changed to string | null | undefined +let aLetExpectingStringOrNullOrUndefinedAfterVariableDeclaration: string | null | undefined; +aLetExpectingStringOrNullOrUndefinedAfterVariableDeclaration = nullableIndexedRecord.a; // ok: Assigning index property 'a' to string or null or undefined variable, 'a' might not be present + +/* + * When noUncheckedIndexedAccess is enabled, TSC will treat property access on aImplicitType as an error, but no-unchecked-record-access causes an error if an index signature is not typed to allow undefined. + */ +const aImplicitType = nullableIndexedRecord.a; // defect: Index property without an explicit undefined can not be assigned to an inferred type +AFnExpectsStringOrNull(aImplicitType); // ok: AFnExpectsStringOrNull(aImplicitType) is the continuation of the inferred type case and should be caught in the variable initialization diff --git a/common/build/eslint-plugin-fluid/src/test/example/no-unchecked-record-access/staticTypes.ts b/common/build/eslint-plugin-fluid/src/test/example/no-unchecked-record-access/staticTypes.ts new file mode 100644 index 000000000000..5971cf8cc47b --- /dev/null +++ b/common/build/eslint-plugin-fluid/src/test/example/no-unchecked-record-access/staticTypes.ts @@ -0,0 +1,28 @@ +/*! + * Copyright (c) Microsoft Corporation and contributors. All rights reserved. + * Licensed under the MIT License. + */ + +/* + * Static Types + * no-unchecked-record-access should not apply to static types, since they are guaranteed to have the properties they define. + */ +/* Constants and Variables */ +type StaticType = { a: string; b: string }; +const someObjWithStaticType: StaticType = { a: "hello", b: "goodbye" }; +const a = "a"; +const b = "b"; + +someObjWithStaticType.a; // ok: Accessing string property 'a' +someObjWithStaticType.a.length; // ok: Accessing length of string property 'a' +someObjWithStaticType["a"]; // ok: Accessing property 'a' using bracket notation +someObjWithStaticType["a"].length; // ok: Accessing length of string property 'a' using bracket notation +someObjWithStaticType[a].length; // ok: Accessing length of string property 'a' using bracket notation +someObjWithStaticType[b].length; // ok: Accessing length of string property 'b' using bracket notation +const aExpectingStringFromStaticType: string = someObjWithStaticType.a; // ok: Assigning string property to a strict string variable +const aExpectingStringOrUndefinedFromStaticType: string | undefined = someObjWithStaticType.a; // ok: Assigning string property to a string or undefined variable + +/* Inferred Static Types */ +const record = { a: 1, b: 2 }; +const recordA = record.a; // ok: Accessing number property 'a' directly +const recordB = record.b; // ok: Accessing number property 'b' directly diff --git a/common/build/eslint-plugin-fluid/src/test/example/no-unchecked-record-access/undefinableIndexedRecord.ts b/common/build/eslint-plugin-fluid/src/test/example/no-unchecked-record-access/undefinableIndexedRecord.ts new file mode 100644 index 000000000000..a9d07791adc4 --- /dev/null +++ b/common/build/eslint-plugin-fluid/src/test/example/no-unchecked-record-access/undefinableIndexedRecord.ts @@ -0,0 +1,37 @@ +/*! + * Copyright (c) Microsoft Corporation and contributors. All rights reserved. + * Licensed under the MIT License. + */ + +/* + * Undefinable index signature + */ + +/* Constants and Variables */ +type UndefinableIndexSignatureType = { [key: string]: string | undefined }; +const undefinableIndexedRecord: UndefinableIndexSignatureType = { a: "hello", b: undefined }; + +/* Function Calls */ +function recordAFnExpectsStringOrUndefined( + record: UndefinableIndexSignatureType, +): string | undefined { + return record.a; // ok: Returning index property 'a' as string or undefined variable should be not be caught, 'a' might be undefined +} + +function AFnExpectsStringOrUndefined(a: string | undefined): string | undefined { + return a; +} + +AFnExpectsStringOrUndefined(undefinableIndexedRecord.a); // ok: Passing index property 'a' to a function that accepts undefined is fine + +/* + * Variable Assignments + */ + +const aExpectingStringOrUndefined: string | undefined = undefinableIndexedRecord.a; // ok: Assigning index property 'a' to string or undefined variable, 'a' might not be present +let aLetExpectingStringOrUndefined: string | undefined = undefinableIndexedRecord.a; // ok: Assigning index property 'a' to string or undefined variable, 'a' might not be present + +const aImplicitType = undefinableIndexedRecord.a; // ok: Index property with union type undefined is allowed to be assigned to inferred type + +let aLetExpectingStringOrUndefinedAfterVariableDeclaration: string | undefined; +aLetExpectingStringOrUndefinedAfterVariableDeclaration = undefinableIndexedRecord.a; // ok: Assigning index property 'a' to string or undefined variable, 'a' might not be present diff --git a/common/build/eslint-plugin-fluid/src/test/example/tsconfig.json b/common/build/eslint-plugin-fluid/src/test/example/tsconfig.json new file mode 100644 index 000000000000..05fd5add9e84 --- /dev/null +++ b/common/build/eslint-plugin-fluid/src/test/example/tsconfig.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "target": "ESNext", + "module": "NodeNext", + "strict": true, + "allowImportingTsExtensions": true, + "noEmit": true, + }, + "include": ["./**/*"], +} diff --git a/common/build/eslint-plugin-fluid/src/test/tsconfig.json b/common/build/eslint-plugin-fluid/src/test/tsconfig.json deleted file mode 100644 index 02b1e1d93ac2..000000000000 --- a/common/build/eslint-plugin-fluid/src/test/tsconfig.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "compilerOptions": { - "rootDir": "./mockFiles", - "target": "ES5", - "composite": true, - }, - "include": ["./mockFiles/**/*"], -} diff --git a/docs/api-markdown-documenter/api-documentation-layout.js b/docs/api-markdown-documenter/api-documentation-layout.js index 89dde3890120..3dc3b834d8ae 100644 --- a/docs/api-markdown-documenter/api-documentation-layout.js +++ b/docs/api-markdown-documenter/api-documentation-layout.js @@ -32,25 +32,6 @@ const supportDocsLinkSpan = new SpanNode([ new PlainTextNode("."), ]); -/** - * Checks if the provided API item or any ancestors is tagged with the specified - * {@link https://tsdoc.org/pages/spec/tag_kinds/#modifier-tags | modifier tag}. - * - * @param apiItem - The API item whose documentation is being queried. - * @param tagName - The TSDoc tag name being queried for. - * Must be a valid TSDoc tag (including starting with `@`). - * - * @throws If the provided TSDoc tag name is invalid. - * - * @privateRemarks import from `@fluid-tools/api-markdown-documenter` once available. - */ -export function ancestryHasModifierTag(apiItem, tagName) { - return ( - ApiItemUtilities.hasModifierTag(apiItem, tagName) || - (apiItem.parent !== undefined && ancestryHasModifierTag(apiItem.parent, tagName)) - ); -} - /** * Creates a special import notice for the provided API item, if one is appropriate. * @@ -112,7 +93,7 @@ function createImportNotice(apiItem) { * If the item is tagged as "@system", displays an internal notice with use notes. */ function createSystemNotice(apiItem) { - if (ancestryHasModifierTag(apiItem, "@system")) { + if (ApiItemUtilities.ancestryHasModifierTag(apiItem, "@system")) { return new AlertNode( [supportDocsLinkSpan], /* alertKind: */ "warning", diff --git a/docs/api-markdown-documenter/render-api-documentation.js b/docs/api-markdown-documenter/render-api-documentation.js index d4d0dcbd8270..f13027581fea 100644 --- a/docs/api-markdown-documenter/render-api-documentation.js +++ b/docs/api-markdown-documenter/render-api-documentation.js @@ -19,7 +19,7 @@ import fs from "fs-extra"; import path from "path"; import { alertNodeType } from "./alert-node.js"; -import { ancestryHasModifierTag, layoutContent } from "./api-documentation-layout.js"; +import { layoutContent } from "./api-documentation-layout.js"; import { buildNavBar } from "./build-api-nav.js"; import { renderAlertNode, renderBlockQuoteNode, renderTableNode } from "./custom-renderers.js"; import { createHugoFrontMatter } from "./front-matter.js"; @@ -58,7 +58,7 @@ export async function renderApiDocumentation(inputDir, outputDir, uriRootDir, ap // Process API reports logProgress("Loading API model..."); - const apiModel = await loadModel(inputDir); + const apiModel = await loadModel({ modelDirectoryPath: inputDir }); // Custom renderers that utilize Hugo syntax for certain kinds of documentation elements. const customRenderers = { @@ -83,7 +83,7 @@ export async function renderApiDocumentation(inputDir, outputDir, uriRootDir, ap createDefaultLayout: layoutContent, getAlertsForItem: (apiItem) => { const alerts = []; - if (ancestryHasModifierTag(apiItem, "@system")) { + if (ApiItemUtilities.ancestryHasModifierTag(apiItem, "@system")) { alerts.push("System"); } else { if (ApiItemUtilities.isDeprecated(apiItem)) { diff --git a/docs/content/docs/build/releases-and-apitags.md b/docs/content/docs/build/releases-and-apitags.md index 3f2f12744876..08cc2fe53064 100644 --- a/docs/content/docs/build/releases-and-apitags.md +++ b/docs/content/docs/build/releases-and-apitags.md @@ -40,16 +40,31 @@ There are no guarantees for API breakages or Fluid document compatibility among For packages that are part of the `@fluidframework` scope and the `fluid-framework` package, we use import paths to communicate the stability and guarantees associated with those APIs. - **Public APIs** - These APIs are officially supported and any breaking changes to these APIs will be done in a major release and go through a deprecation phase before being removed. -- **Beta APIs** (`/beta` import path) - These APIs are on the path to being officially supported but can still change before becoming a Public API in a future release. They are meant as a preview for developers to experiment with and provide feedback. These APIs can be changed in minor releases. Production usage of these APIs is discouraged. -- **Legacy APIs** (`/legacy` import path) - These APIs were used by the early adopters of Fluid Framework, and we strongly discourage new applications from using these APIs. We will continue to support use of SharedMap & SharedDirectory DDSes until we provide a migration path to SharedTree in a future release. - - For existing users of these Legacy APIs, you will have to use the /legacy import path. This is intentional to highlight that we do not encourage new development using these APIs and plan to provide a graceful path away from them in future. +- **Beta APIs** (`/beta` import path) - These APIs are on the path to being officially supported but can still change before becoming a Public API in a future release. + They are meant as a preview for developers to experiment with and provide feedback. + These APIs can be changed in minor releases. + Production usage of these APIs is discouraged. + - For JavaScript users, such APIs can be identified via the `@beta` tag included in their documentation. +- **Legacy APIs** (`/legacy` import path) - These APIs were used by the early adopters of Fluid Framework, and we strongly discourage new applications from using these APIs. + We will continue to support use of SharedMap & SharedDirectory DDSes until we provide a migration path to SharedTree in a future release. + - For existing users of these Legacy APIs, you will have to use the /legacy import path. + This is intentional to highlight that we do not encourage new development using these APIs and plan to provide a graceful path away from them in future. - For example, SharedMap is now a Legacy API and should be imported as follows: ```typescript import { SharedMap } from "fluid-framework/legacy" ``` -- **System APIs** - These APIs are reserved for internal system use and are not meant to be used directly. These may change at any time without notice. In cases that a type must be referenced, the contents should never be inspected. + - For JavaScript users, such APIs can be identified via the `@legacy` tag included in their documentation. +- **System APIs** - These APIs are reserved for internal system use and are not meant to be used directly. + These may change at any time without notice. + For cases in which such a type must be referenced, the contents should never be inspected. + - For JavaScript users, such APIs can be identified via the `@system` tag included in their documentation. +- **Internal APIs** - These APIs are *strictly* for internal system use and should not be used. + These may change at any time without notice. + - Do not import any APIs from the `/internal` import path. + - For JavaScript users, such APIs can be identified via the `@internal` tag included in their documentation. + Such APIs should not be used. There are no API stability guarantees for packages in the `@fluid-experimental` and `@fluid-internal` scopes. `@fluid-experimental` APIs are for developers to try experimental features and provide feedback. It's possible that these APIs can get completely scrapped or drastically changed in a minor release based on developer feedback. diff --git a/docs/package.json b/docs/package.json index 0610bbc2be6d..6991e6d7feb3 100644 --- a/docs/package.json +++ b/docs/package.json @@ -39,7 +39,7 @@ "start": "hugo server" }, "dependencies": { - "@fluid-tools/api-markdown-documenter": "^0.15.0", + "@fluid-tools/api-markdown-documenter": "^0.16.0", "@fluid-tools/markdown-magic": "file:../tools/markdown-magic", "@fluidframework/build-common": "^2.0.3", "@rushstack/node-core-library": "^4.0.2", diff --git a/docs/pnpm-lock.yaml b/docs/pnpm-lock.yaml index f9d297496550..a8b28222765e 100644 --- a/docs/pnpm-lock.yaml +++ b/docs/pnpm-lock.yaml @@ -9,8 +9,8 @@ importers: .: dependencies: '@fluid-tools/api-markdown-documenter': - specifier: ^0.15.0 - version: 0.15.0 + specifier: ^0.16.0 + version: 0.16.0 '@fluid-tools/markdown-magic': specifier: file:../tools/markdown-magic version: file:../tools/markdown-magic(markdown-magic@2.6.1) @@ -143,8 +143,8 @@ packages: regenerator-runtime: 0.13.11 dev: false - /@fluid-tools/api-markdown-documenter@0.15.0: - resolution: {integrity: sha512-6vRyuVLFDjKSb9D1Qi4kjq1og+e8gCdeTnJZSKapfSdoIqywVFsTdo9avz0ay1tW/H6z3hZaq+DxeYX2BsK+mA==} + /@fluid-tools/api-markdown-documenter@0.16.0: + resolution: {integrity: sha512-A8tVCxr296QgoKLeZHcZtf5hClffpWvI2L2PAl9BnNZYQbVR9TOAXWBDi4Py0E42Mes10LyscO+FFvbVHKISbw==} dependencies: '@microsoft/api-extractor-model': 7.28.2 '@microsoft/tsdoc': 0.14.2 diff --git a/docs/skipped-urls.txt b/docs/skipped-urls.txt index 289827c7b833..fc7539d6bb0c 100644 --- a/docs/skipped-urls.txt +++ b/docs/skipped-urls.txt @@ -9,6 +9,12 @@ https://twitter.com/intent/follow* https://twitter.com/intent/tweet* https://twitter.com/fluidframework https://c.s-microsoft.com/static/fonts/segoe-ui/west-european/* +# GitHub returns 429 +https://github.com/microsoft/FluidFramework/issues/* + +# denied by robots.txt +https://aka.ms/* +https://go.microsoft.com/fwlink/* # These URLs have false positives with their anchors. Linkcheck thinks the anchors are missing. https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort* diff --git a/examples/apps/attributable-map/CHANGELOG.md b/examples/apps/attributable-map/CHANGELOG.md index c81702363c6d..9906965cf5ea 100644 --- a/examples/apps/attributable-map/CHANGELOG.md +++ b/examples/apps/attributable-map/CHANGELOG.md @@ -1,5 +1,9 @@ # @fluid-example/attributable-map +## 2.2.0 + +Dependency updates only. + ## 2.1.0 Dependency updates only. diff --git a/examples/apps/attributable-map/package.json b/examples/apps/attributable-map/package.json index 9c3e960e1ee7..9f0d9cdb9e5b 100644 --- a/examples/apps/attributable-map/package.json +++ b/examples/apps/attributable-map/package.json @@ -1,6 +1,6 @@ { "name": "@fluid-example/attributable-map", - "version": "2.2.0", + "version": "2.3.0", "private": true, "description": "Minimal Fluid Container & Data Object sample to implement a hit counter as a standalone app.", "homepage": "https://fluidframework.com", @@ -46,9 +46,9 @@ }, "devDependencies": { "@biomejs/biome": "~1.8.3", - "@fluid-tools/build-cli": "0.43.0-285387", + "@fluid-tools/build-cli": "^0.44.0", "@fluidframework/build-common": "^2.0.3", - "@fluidframework/build-tools": "0.43.0-285387", + "@fluidframework/build-tools": "^0.44.0", "@fluidframework/eslint-config-fluid": "^5.3.0", "@types/node": "^18.19.0", "eslint": "~8.55.0", diff --git a/examples/apps/attributable-map/src/modelContainerRuntimeFactoryWithAttribution.ts b/examples/apps/attributable-map/src/modelContainerRuntimeFactoryWithAttribution.ts index 0ebef120cff8..841f8ef247ce 100644 --- a/examples/apps/attributable-map/src/modelContainerRuntimeFactoryWithAttribution.ts +++ b/examples/apps/attributable-map/src/modelContainerRuntimeFactoryWithAttribution.ts @@ -4,7 +4,7 @@ */ import { IModelContainerRuntimeEntryPoint } from "@fluid-example/example-utils"; -import { createRuntimeAttributor, mixinAttributor } from "@fluid-experimental/attributor"; +import { mixinAttributor } from "@fluid-experimental/attributor"; import { IContainer, IContainerContext, @@ -54,7 +54,6 @@ export abstract class ModelContainerRuntimeFactoryWithAttribution this.createModel(containerRuntime, container), }), runtimeOptions: this.runtimeOptions, - containerScope: { IRuntimeAttributor: createRuntimeAttributor() }, existing, }); diff --git a/examples/apps/collaborative-textarea/CHANGELOG.md b/examples/apps/collaborative-textarea/CHANGELOG.md index f0e02be8ad4f..81492e307c40 100644 --- a/examples/apps/collaborative-textarea/CHANGELOG.md +++ b/examples/apps/collaborative-textarea/CHANGELOG.md @@ -1,5 +1,9 @@ # @fluid-example/collaborative-textarea +## 2.2.0 + +Dependency updates only. + ## 2.1.0 Dependency updates only. diff --git a/examples/apps/collaborative-textarea/package.json b/examples/apps/collaborative-textarea/package.json index 5c3e0e0c1058..ccfe51d74235 100644 --- a/examples/apps/collaborative-textarea/package.json +++ b/examples/apps/collaborative-textarea/package.json @@ -1,6 +1,6 @@ { "name": "@fluid-example/collaborative-textarea", - "version": "2.2.0", + "version": "2.3.0", "private": true, "description": "A minimal example using the react collaborative-textarea", "homepage": "https://fluidframework.com", @@ -59,9 +59,9 @@ }, "devDependencies": { "@biomejs/biome": "~1.8.3", - "@fluid-tools/build-cli": "0.43.0-285387", + "@fluid-tools/build-cli": "^0.44.0", "@fluidframework/build-common": "^2.0.3", - "@fluidframework/build-tools": "0.43.0-285387", + "@fluidframework/build-tools": "^0.44.0", "@fluidframework/eslint-config-fluid": "^5.3.0", "@fluidframework/test-tools": "^1.0.195075", "@fluidframework/test-utils": "workspace:~", diff --git a/examples/apps/contact-collection/CHANGELOG.md b/examples/apps/contact-collection/CHANGELOG.md index da896dbd6193..91e65dde4fdf 100644 --- a/examples/apps/contact-collection/CHANGELOG.md +++ b/examples/apps/contact-collection/CHANGELOG.md @@ -1,5 +1,9 @@ # @fluid-example/contact-collection +## 2.2.0 + +Dependency updates only. + ## 2.1.0 Dependency updates only. diff --git a/examples/apps/contact-collection/package.json b/examples/apps/contact-collection/package.json index d9391bc767ef..6c7991bfe9bd 100644 --- a/examples/apps/contact-collection/package.json +++ b/examples/apps/contact-collection/package.json @@ -1,6 +1,6 @@ { "name": "@fluid-example/contact-collection", - "version": "2.2.0", + "version": "2.3.0", "private": true, "description": "Example of using a Fluid Object as a collection of items", "homepage": "https://fluidframework.com", @@ -51,9 +51,9 @@ }, "devDependencies": { "@biomejs/biome": "~1.8.3", - "@fluid-tools/build-cli": "0.43.0-285387", + "@fluid-tools/build-cli": "^0.44.0", "@fluidframework/build-common": "^2.0.3", - "@fluidframework/build-tools": "0.43.0-285387", + "@fluidframework/build-tools": "^0.44.0", "@fluidframework/eslint-config-fluid": "^5.3.0", "@fluidframework/test-tools": "^1.0.195075", "@types/jest": "29.5.3", diff --git a/examples/apps/data-object-grid/CHANGELOG.md b/examples/apps/data-object-grid/CHANGELOG.md index f2ac8cf717e2..8ade906a723d 100644 --- a/examples/apps/data-object-grid/CHANGELOG.md +++ b/examples/apps/data-object-grid/CHANGELOG.md @@ -1,5 +1,9 @@ # @fluid-example/data-object-grid +## 2.2.0 + +Dependency updates only. + ## 2.1.0 Dependency updates only. diff --git a/examples/apps/data-object-grid/package.json b/examples/apps/data-object-grid/package.json index 47fc41249620..6e58d2c13bbc 100644 --- a/examples/apps/data-object-grid/package.json +++ b/examples/apps/data-object-grid/package.json @@ -1,6 +1,6 @@ { "name": "@fluid-example/data-object-grid", - "version": "2.2.0", + "version": "2.3.0", "private": true, "description": "Data object grid creates child data objects from a registry and lays them out in a grid.", "homepage": "https://fluidframework.com", @@ -65,9 +65,9 @@ }, "devDependencies": { "@biomejs/biome": "~1.8.3", - "@fluid-tools/build-cli": "0.43.0-285387", + "@fluid-tools/build-cli": "^0.44.0", "@fluidframework/build-common": "^2.0.3", - "@fluidframework/build-tools": "0.43.0-285387", + "@fluidframework/build-tools": "^0.44.0", "@fluidframework/eslint-config-fluid": "^5.3.0", "@fluidframework/test-tools": "^1.0.195075", "@types/jest": "29.5.3", diff --git a/examples/apps/presence-tracker/CHANGELOG.md b/examples/apps/presence-tracker/CHANGELOG.md index f267871ded11..74a21418f6f2 100644 --- a/examples/apps/presence-tracker/CHANGELOG.md +++ b/examples/apps/presence-tracker/CHANGELOG.md @@ -1,5 +1,9 @@ # @fluid-example/presence-tracker +## 2.2.0 + +Dependency updates only. + ## 2.1.0 Dependency updates only. diff --git a/examples/apps/presence-tracker/package.json b/examples/apps/presence-tracker/package.json index 0427ea8b9786..f25ebcdb0ac8 100644 --- a/examples/apps/presence-tracker/package.json +++ b/examples/apps/presence-tracker/package.json @@ -1,6 +1,6 @@ { "name": "@fluid-example/presence-tracker", - "version": "2.2.0", + "version": "2.3.0", "private": true, "description": "Example Data Object that tracks page focus for Audience members using signals.", "homepage": "https://fluidframework.com", @@ -52,9 +52,9 @@ }, "devDependencies": { "@biomejs/biome": "~1.8.3", - "@fluid-tools/build-cli": "0.43.0-285387", + "@fluid-tools/build-cli": "^0.44.0", "@fluidframework/build-common": "^2.0.3", - "@fluidframework/build-tools": "0.43.0-285387", + "@fluidframework/build-tools": "^0.44.0", "@fluidframework/eslint-config-fluid": "^5.3.0", "@fluidframework/test-tools": "^1.0.195075", "@types/jest": "29.5.3", diff --git a/examples/apps/task-selection/CHANGELOG.md b/examples/apps/task-selection/CHANGELOG.md index c90ab5b47366..fea75b0a9e04 100644 --- a/examples/apps/task-selection/CHANGELOG.md +++ b/examples/apps/task-selection/CHANGELOG.md @@ -1,5 +1,9 @@ # @fluid-example/task-selection +## 2.2.0 + +Dependency updates only. + ## 2.1.0 Dependency updates only. diff --git a/examples/apps/task-selection/package.json b/examples/apps/task-selection/package.json index fda0086372ec..0f3294a60cba 100644 --- a/examples/apps/task-selection/package.json +++ b/examples/apps/task-selection/package.json @@ -1,6 +1,6 @@ { "name": "@fluid-example/task-selection", - "version": "2.2.0", + "version": "2.3.0", "private": true, "description": "Example demonstrating selecting a unique task amongst connected Fluid clients", "homepage": "https://fluidframework.com", @@ -54,9 +54,9 @@ }, "devDependencies": { "@biomejs/biome": "~1.8.3", - "@fluid-tools/build-cli": "0.43.0-285387", + "@fluid-tools/build-cli": "^0.44.0", "@fluidframework/build-common": "^2.0.3", - "@fluidframework/build-tools": "0.43.0-285387", + "@fluidframework/build-tools": "^0.44.0", "@fluidframework/eslint-config-fluid": "^5.3.0", "@fluidframework/test-tools": "^1.0.195075", "@types/jest": "29.5.3", diff --git a/examples/apps/tree-comparison/CHANGELOG.md b/examples/apps/tree-comparison/CHANGELOG.md index 066a95b836af..186675f2a299 100644 --- a/examples/apps/tree-comparison/CHANGELOG.md +++ b/examples/apps/tree-comparison/CHANGELOG.md @@ -1,5 +1,9 @@ # @fluid-example/tree-comparison +## 2.2.0 + +Dependency updates only. + ## 2.1.0 Dependency updates only. diff --git a/examples/apps/tree-comparison/package.json b/examples/apps/tree-comparison/package.json index 0b3d0c5dc783..1d0ccdb3eb33 100644 --- a/examples/apps/tree-comparison/package.json +++ b/examples/apps/tree-comparison/package.json @@ -1,6 +1,6 @@ { "name": "@fluid-example/tree-comparison", - "version": "2.2.0", + "version": "2.3.0", "private": true, "description": "Comparing API usage in legacy SharedTree and new SharedTree.", "homepage": "https://fluidframework.com", @@ -60,9 +60,9 @@ }, "devDependencies": { "@biomejs/biome": "~1.8.3", - "@fluid-tools/build-cli": "0.43.0-285387", + "@fluid-tools/build-cli": "^0.44.0", "@fluidframework/build-common": "^2.0.3", - "@fluidframework/build-tools": "0.43.0-285387", + "@fluidframework/build-tools": "^0.44.0", "@fluidframework/eslint-config-fluid": "^5.3.0", "@fluidframework/test-tools": "^1.0.195075", "@types/jest": "29.5.3", diff --git a/examples/benchmarks/bubblebench/baseline/CHANGELOG.md b/examples/benchmarks/bubblebench/baseline/CHANGELOG.md index e34ea3e62802..e3e3a3e8e378 100644 --- a/examples/benchmarks/bubblebench/baseline/CHANGELOG.md +++ b/examples/benchmarks/bubblebench/baseline/CHANGELOG.md @@ -1,5 +1,9 @@ # @fluid-example/bubblebench-baseline +## 2.2.0 + +Dependency updates only. + ## 2.1.0 Dependency updates only. diff --git a/examples/benchmarks/bubblebench/baseline/package.json b/examples/benchmarks/bubblebench/baseline/package.json index 3df2b4c08f32..6b002bdfd849 100644 --- a/examples/benchmarks/bubblebench/baseline/package.json +++ b/examples/benchmarks/bubblebench/baseline/package.json @@ -1,6 +1,6 @@ { "name": "@fluid-example/bubblebench-baseline", - "version": "2.2.0", + "version": "2.3.0", "private": true, "description": "Bubblemark inspired DDS benchmark", "homepage": "https://fluidframework.com", @@ -54,7 +54,7 @@ "@biomejs/biome": "~1.8.3", "@fluid-example/webpack-fluid-loader": "workspace:~", "@fluidframework/build-common": "^2.0.3", - "@fluidframework/build-tools": "0.43.0-285387", + "@fluidframework/build-tools": "^0.44.0", "@fluidframework/eslint-config-fluid": "^5.3.0", "@fluidframework/test-tools": "^1.0.195075", "@types/jest": "29.5.3", diff --git a/examples/benchmarks/bubblebench/common/CHANGELOG.md b/examples/benchmarks/bubblebench/common/CHANGELOG.md index 44488dc8cc42..bbc45840909f 100644 --- a/examples/benchmarks/bubblebench/common/CHANGELOG.md +++ b/examples/benchmarks/bubblebench/common/CHANGELOG.md @@ -1,5 +1,9 @@ # @fluid-example/bubblebench-common +## 2.2.0 + +Dependency updates only. + ## 2.1.0 Dependency updates only. diff --git a/examples/benchmarks/bubblebench/common/package.json b/examples/benchmarks/bubblebench/common/package.json index a915b237aeb3..8876ece8e1f3 100644 --- a/examples/benchmarks/bubblebench/common/package.json +++ b/examples/benchmarks/bubblebench/common/package.json @@ -1,6 +1,6 @@ { "name": "@fluid-example/bubblebench-common", - "version": "2.2.0", + "version": "2.3.0", "private": true, "description": "Bubblemark inspired DDS benchmark", "homepage": "https://fluidframework.com", @@ -52,9 +52,9 @@ }, "devDependencies": { "@biomejs/biome": "~1.8.3", - "@fluid-tools/build-cli": "0.43.0-285387", + "@fluid-tools/build-cli": "^0.44.0", "@fluidframework/build-common": "^2.0.3", - "@fluidframework/build-tools": "0.43.0-285387", + "@fluidframework/build-tools": "^0.44.0", "@fluidframework/eslint-config-fluid": "^5.3.0", "@types/react": "^17.0.44", "@types/react-dom": "^17.0.18", diff --git a/examples/benchmarks/bubblebench/experimental-tree/CHANGELOG.md b/examples/benchmarks/bubblebench/experimental-tree/CHANGELOG.md index 9606ec9ed1f6..416458643eb5 100644 --- a/examples/benchmarks/bubblebench/experimental-tree/CHANGELOG.md +++ b/examples/benchmarks/bubblebench/experimental-tree/CHANGELOG.md @@ -1,5 +1,9 @@ # @fluid-example/bubblebench-experimental-tree +## 2.2.0 + +Dependency updates only. + ## 2.1.0 Dependency updates only. diff --git a/examples/benchmarks/bubblebench/experimental-tree/package.json b/examples/benchmarks/bubblebench/experimental-tree/package.json index e126413f496b..c5ed56f7fe2d 100644 --- a/examples/benchmarks/bubblebench/experimental-tree/package.json +++ b/examples/benchmarks/bubblebench/experimental-tree/package.json @@ -1,6 +1,6 @@ { "name": "@fluid-example/bubblebench-experimental-tree", - "version": "2.2.0", + "version": "2.3.0", "private": true, "description": "Bubblemark inspired DDS benchmark", "homepage": "https://fluidframework.com", @@ -55,7 +55,7 @@ "@biomejs/biome": "~1.8.3", "@fluid-example/webpack-fluid-loader": "workspace:~", "@fluidframework/build-common": "^2.0.3", - "@fluidframework/build-tools": "0.43.0-285387", + "@fluidframework/build-tools": "^0.44.0", "@fluidframework/eslint-config-fluid": "^5.3.0", "@fluidframework/test-tools": "^1.0.195075", "@types/jest": "29.5.3", diff --git a/examples/benchmarks/bubblebench/ot/CHANGELOG.md b/examples/benchmarks/bubblebench/ot/CHANGELOG.md index 41da130d500f..9a9ca8405da3 100644 --- a/examples/benchmarks/bubblebench/ot/CHANGELOG.md +++ b/examples/benchmarks/bubblebench/ot/CHANGELOG.md @@ -1,5 +1,9 @@ # @fluid-example/bubblebench-ot +## 2.2.0 + +Dependency updates only. + ## 2.1.0 Dependency updates only. diff --git a/examples/benchmarks/bubblebench/ot/package.json b/examples/benchmarks/bubblebench/ot/package.json index 28ed9382c49d..71c89e144a40 100644 --- a/examples/benchmarks/bubblebench/ot/package.json +++ b/examples/benchmarks/bubblebench/ot/package.json @@ -1,6 +1,6 @@ { "name": "@fluid-example/bubblebench-ot", - "version": "2.2.0", + "version": "2.3.0", "private": true, "description": "Bubblemark inspired DDS benchmark", "homepage": "https://fluidframework.com", @@ -56,7 +56,7 @@ "@biomejs/biome": "~1.8.3", "@fluid-example/webpack-fluid-loader": "workspace:~", "@fluidframework/build-common": "^2.0.3", - "@fluidframework/build-tools": "0.43.0-285387", + "@fluidframework/build-tools": "^0.44.0", "@fluidframework/eslint-config-fluid": "^5.3.0", "@fluidframework/test-tools": "^1.0.195075", "@types/jest": "29.5.3", diff --git a/examples/benchmarks/bubblebench/shared-tree/CHANGELOG.md b/examples/benchmarks/bubblebench/shared-tree/CHANGELOG.md index c2189db94943..7cb5b4b773b8 100644 --- a/examples/benchmarks/bubblebench/shared-tree/CHANGELOG.md +++ b/examples/benchmarks/bubblebench/shared-tree/CHANGELOG.md @@ -1,5 +1,9 @@ # @fluid-example/bubblebench-simple-tree +## 2.2.0 + +Dependency updates only. + ## 2.1.0 Dependency updates only. diff --git a/examples/benchmarks/bubblebench/shared-tree/package.json b/examples/benchmarks/bubblebench/shared-tree/package.json index 96bb2ba00a47..369390b4adb5 100644 --- a/examples/benchmarks/bubblebench/shared-tree/package.json +++ b/examples/benchmarks/bubblebench/shared-tree/package.json @@ -1,6 +1,6 @@ { "name": "@fluid-example/bubblebench-shared-tree", - "version": "2.2.0", + "version": "2.3.0", "private": true, "description": "Bubblemark inspired DDS benchmark", "homepage": "https://fluidframework.com", @@ -63,7 +63,7 @@ "@biomejs/biome": "~1.8.3", "@fluid-example/webpack-fluid-loader": "workspace:~", "@fluidframework/build-common": "^2.0.3", - "@fluidframework/build-tools": "0.43.0-285387", + "@fluidframework/build-tools": "^0.44.0", "@fluidframework/eslint-config-fluid": "^5.3.0", "@fluidframework/test-tools": "^1.0.195075", "@types/jest": "29.5.3", diff --git a/examples/benchmarks/odspsnapshotfetch-perftestapp/CHANGELOG.md b/examples/benchmarks/odspsnapshotfetch-perftestapp/CHANGELOG.md index 0289efa42d88..61ed09f8c4ef 100644 --- a/examples/benchmarks/odspsnapshotfetch-perftestapp/CHANGELOG.md +++ b/examples/benchmarks/odspsnapshotfetch-perftestapp/CHANGELOG.md @@ -1,5 +1,9 @@ # @fluid-example/odspsnapshotfetch-perftestapp +## 2.2.0 + +Dependency updates only. + ## 2.1.0 Dependency updates only. diff --git a/examples/benchmarks/odspsnapshotfetch-perftestapp/package.json b/examples/benchmarks/odspsnapshotfetch-perftestapp/package.json index d8df00d045b6..ebc0d98ea1e3 100644 --- a/examples/benchmarks/odspsnapshotfetch-perftestapp/package.json +++ b/examples/benchmarks/odspsnapshotfetch-perftestapp/package.json @@ -1,6 +1,6 @@ { "name": "@fluid-example/odspsnapshotfetch-perftestapp", - "version": "2.2.0", + "version": "2.3.0", "private": true, "description": "Benchmark binary vs. json download", "homepage": "https://fluidframework.com", @@ -48,9 +48,9 @@ }, "devDependencies": { "@biomejs/biome": "~1.8.3", - "@fluid-tools/build-cli": "0.43.0-285387", + "@fluid-tools/build-cli": "^0.44.0", "@fluidframework/build-common": "^2.0.3", - "@fluidframework/build-tools": "0.43.0-285387", + "@fluidframework/build-tools": "^0.44.0", "@fluidframework/eslint-config-fluid": "^5.3.0", "@types/express": "^4.17.21", "@types/fs-extra": "^9.0.11", diff --git a/examples/benchmarks/tablebench/.gitignore b/examples/benchmarks/tablebench/.gitignore index ea5ba490f548..c48e8089d1ef 100644 --- a/examples/benchmarks/tablebench/.gitignore +++ b/examples/benchmarks/tablebench/.gitignore @@ -52,4 +52,7 @@ intel_modules/ temp_modules/ # Fuzz test operation files -**/fuzz/failures/** \ No newline at end of file +**/fuzz/failures/** + +# Output folder for custom benchmark tests +.customBenchmarksOutput diff --git a/examples/benchmarks/tablebench/.mocharc.customBenchmarks.cjs b/examples/benchmarks/tablebench/.mocharc.customBenchmarks.cjs new file mode 100644 index 000000000000..d39d2a8d057a --- /dev/null +++ b/examples/benchmarks/tablebench/.mocharc.customBenchmarks.cjs @@ -0,0 +1,33 @@ +/*! + * Copyright (c) Microsoft Corporation and contributors. All rights reserved. + * Licensed under the MIT License. + */ + +/** + * Mocha configuration file to run memory-profiling tests + */ +"use strict"; + +const getFluidTestMochaConfig = require("@fluid-internal/mocha-test-setup/mocharc-common"); + +const packageDir = __dirname; +const baseConfig = getFluidTestMochaConfig(packageDir); + +const nodeOptions = + baseConfig["node-option"] !== undefined + ? Array.isArray(baseConfig["node-option"]) + ? baseConfig["node-option"] + : [baseConfig["node-option"]] // If string, wrap as array to use spread operator + : []; // If undefined, use an empty array + +nodeOptions.push("expose-gc", "gc-global", "unhandled-rejections=strict"); + +module.exports = { + ...baseConfig, + "fgrep": ["@CustomBenchmark"], + "node-option": nodeOptions, // without leading "--" + "recursive": true, + "reporter": "@fluid-tools/benchmark/dist/MochaReporter.js", + "reporterOptions": ["reportDir=.customBenchmarksOutput/"], + "spec": ["lib/test/**/*.*js"], +}; diff --git a/examples/benchmarks/tablebench/CHANGELOG.md b/examples/benchmarks/tablebench/CHANGELOG.md index ad522bd95995..40ca76ff8918 100644 --- a/examples/benchmarks/tablebench/CHANGELOG.md +++ b/examples/benchmarks/tablebench/CHANGELOG.md @@ -1,5 +1,9 @@ # @fluid-internal/tablebench +## 2.2.0 + +Dependency updates only. + ## 2.1.0 Dependency updates only. diff --git a/examples/benchmarks/tablebench/package.json b/examples/benchmarks/tablebench/package.json index 5990ebfd56f9..66e9c5762c5c 100644 --- a/examples/benchmarks/tablebench/package.json +++ b/examples/benchmarks/tablebench/package.json @@ -1,6 +1,6 @@ { "name": "@fluid-internal/tablebench", - "version": "2.2.0", + "version": "2.3.0", "private": true, "description": "Table focused benchmarks", "homepage": "https://fluidframework.com", @@ -42,6 +42,7 @@ "start:webpack": "webpack serve --config webpack.config.cjs --env mode=tinylicious", "test": "npm run test:mocha", "test:benchmark:report": "mocha --exit --perfMode --parentProcess --fgrep @Benchmark --reporter @fluid-tools/benchmark/dist/MochaReporter.js --timeout 60000", + "test:customBenchmarks": "mocha --config ./.mocharc.customBenchmarks.cjs", "test:mocha": "npm run test:mocha:esm", "test:mocha:esm": "mocha --exit", "test:mocha:verbose": "cross-env FLUID_TEST_VERBOSE=1 npm run test:mocha", @@ -63,7 +64,7 @@ "@fluid-internal/mocha-test-setup": "workspace:~", "@fluid-tools/benchmark": "^0.50.0", "@fluidframework/build-common": "^2.0.3", - "@fluidframework/build-tools": "0.43.0-285387", + "@fluidframework/build-tools": "^0.44.0", "@fluidframework/eslint-config-fluid": "^5.3.0", "@fluidframework/id-compressor": "workspace:~", "@types/mocha": "^9.1.1", diff --git a/examples/benchmarks/tablebench/src/test/table.bench.spec.ts b/examples/benchmarks/tablebench/src/test/table.bench.spec.ts index 6112e33844d8..2cc0a2ef1e51 100644 --- a/examples/benchmarks/tablebench/src/test/table.bench.spec.ts +++ b/examples/benchmarks/tablebench/src/test/table.bench.spec.ts @@ -3,7 +3,12 @@ * Licensed under the MIT License. */ -import { BenchmarkType, benchmark, isInPerformanceTestingMode } from "@fluid-tools/benchmark"; +import { + BenchmarkType, + benchmark, + benchmarkCustom, + isInPerformanceTestingMode, +} from "@fluid-tools/benchmark"; import { IChannel } from "@fluidframework/datastore-definitions/internal"; import { SharedMatrix } from "@fluidframework/matrix/internal"; import { type ITree, NodeFromSchema, TreeViewConfiguration } from "@fluidframework/tree"; @@ -130,70 +135,75 @@ describe("Table", () => { const colMajorJsonBytes = measureEncodedLength(JSON.stringify(transposeTable(data))); let summaryBytes: number; - // After each test, print the summary size information to the console. - afterEach(() => { - // When using a logger, Mocha suppresses 'console.log()' by default. - // Writing directly to 'process.stdout' bypasses this suppression. - process.stdout.write(` Summary: ${summaryBytes} bytes\n`); - process.stdout.write( - ` vs row-major: ${(summaryBytes / rowMajorJsonBytes).toLocaleString( - undefined, - { - maximumFractionDigits: 2, - minimumFractionDigits: 2, - }, - )}x\n`, - ); - process.stdout.write( - ` vs col-major: ${(summaryBytes / colMajorJsonBytes).toLocaleString( - undefined, - { - maximumFractionDigits: 2, - minimumFractionDigits: 2, - }, - )}x\n`, - ); + benchmarkCustom({ + only: false, + type: BenchmarkType.Measurement, + title: `Row-major JSON (Typical Database Baseline)`, + run: async (reporter) => { + summaryBytes = rowMajorJsonBytes; + reporter.addMeasurement(`summaryBytes`, summaryBytes); + reporter.addMeasurement(`vs row-major:`, summaryBytes / rowMajorJsonBytes); + reporter.addMeasurement(`vs col-major:`, summaryBytes / colMajorJsonBytes); + }, }); - it("Row-major JSON (Typical Database Baseline)", () => { - // Row/col major sizes are precalculated before the test run. - // Copy the value to 'summaryBytes' for reporting by 'afterEach' above. - summaryBytes = rowMajorJsonBytes; + benchmarkCustom({ + only: false, + type: BenchmarkType.Measurement, + title: `Column-major JSON (Compact REST Baseline)`, + run: async (reporter) => { + summaryBytes = colMajorJsonBytes; + reporter.addMeasurement(`summaryBytes`, summaryBytes); + reporter.addMeasurement(`vs row-major:`, summaryBytes / rowMajorJsonBytes); + reporter.addMeasurement(`vs col-major:`, summaryBytes / colMajorJsonBytes); + }, }); - it("Column-major JSON (Compact REST Baseline)", () => { - // Row/col major sizes are precalculated before the test run. - // Copy the value to 'summaryBytes' for reporting by 'afterEach' above. - summaryBytes = colMajorJsonBytes; - }); - - it("SharedMatrix", () => { - const columnNames = Object.keys(data[0]); - - const { channel, processAllMessages } = create(SharedMatrix.getFactory()); - matrix = channel as SharedMatrix; - matrix.insertCols(0, columnNames.length); - matrix.insertRows(0, data.length); - - for (let r = 0; r < data.length; r++) { - for (const [c, key] of columnNames.entries()) { - matrix.setCell(r, c, (data as any)[r][key]); + benchmarkCustom({ + only: false, + type: BenchmarkType.Measurement, + title: `SharedMatrix`, + run: async (reporter) => { + const columnNames = Object.keys(data[0]); + + const { channel, processAllMessages } = create(SharedMatrix.getFactory()); + matrix = channel as SharedMatrix; + matrix.insertCols(0, columnNames.length); + matrix.insertRows(0, data.length); + + for (let r = 0; r < data.length; r++) { + for (const [c, key] of columnNames.entries()) { + matrix.setCell(r, c, (data as any)[r][key]); + } } - } - processAllMessages(); - summaryBytes = measureAttachmentSummary(channel); + processAllMessages(); + summaryBytes = measureAttachmentSummary(channel); + + reporter.addMeasurement(`summaryBytes`, summaryBytes); + reporter.addMeasurement(`vs row-major:`, summaryBytes / rowMajorJsonBytes); + reporter.addMeasurement(`vs col-major:`, summaryBytes / colMajorJsonBytes); + }, }); - it("SharedTree", () => { - const { channel, processAllMessages } = create(SharedTree.getFactory()); - tree = channel; + benchmarkCustom({ + only: false, + type: BenchmarkType.Measurement, + title: `SharedTree`, + run: async (reporter) => { + const { channel, processAllMessages } = create(SharedTree.getFactory()); + tree = channel; - const view = tree.viewWith(new TreeViewConfiguration({ schema: Table })); - view.initialize(data); + const view = tree.viewWith(new TreeViewConfiguration({ schema: Table })); + view.initialize(data); - processAllMessages(); - summaryBytes = measureAttachmentSummary(channel); + processAllMessages(); + summaryBytes = measureAttachmentSummary(channel); + + reporter.addMeasurement(`summaryBytes`, summaryBytes); + reporter.addMeasurement(`vs row-major:`, summaryBytes / rowMajorJsonBytes); + reporter.addMeasurement(`vs col-major:`, summaryBytes / colMajorJsonBytes); + }, }); }); }); diff --git a/examples/client-logger/app-insights-logger/CHANGELOG.md b/examples/client-logger/app-insights-logger/CHANGELOG.md index e516f5f2757e..c74ea31f3d0d 100644 --- a/examples/client-logger/app-insights-logger/CHANGELOG.md +++ b/examples/client-logger/app-insights-logger/CHANGELOG.md @@ -1,5 +1,9 @@ # @fluid-example/app-insights-logger +## 2.2.0 + +Dependency updates only. + ## 2.1.0 Dependency updates only. diff --git a/examples/client-logger/app-insights-logger/package.json b/examples/client-logger/app-insights-logger/package.json index 5e2992f8e77e..e5f9f717360f 100644 --- a/examples/client-logger/app-insights-logger/package.json +++ b/examples/client-logger/app-insights-logger/package.json @@ -1,6 +1,6 @@ { "name": "@fluid-example/app-insights-logger", - "version": "2.2.0", + "version": "2.3.0", "private": true, "description": "Provides a simple Fluid application with a UI view written in React to test the Fluid App Insights telemetry logger that will route typical Fluid telemetry to configured Azure App Insights", "homepage": "https://fluidframework.com", @@ -57,7 +57,7 @@ "devDependencies": { "@biomejs/biome": "~1.8.3", "@fluidframework/build-common": "^2.0.3", - "@fluidframework/build-tools": "0.43.0-285387", + "@fluidframework/build-tools": "^0.44.0", "@fluidframework/eslint-config-fluid": "^5.3.0", "@testing-library/dom": "^8.2.0", "@testing-library/jest-dom": "^5.16.5", diff --git a/examples/data-objects/canvas/CHANGELOG.md b/examples/data-objects/canvas/CHANGELOG.md index 1e9f50493a3a..608fcfc54ee5 100644 --- a/examples/data-objects/canvas/CHANGELOG.md +++ b/examples/data-objects/canvas/CHANGELOG.md @@ -1,5 +1,9 @@ # @fluid-example/canvas +## 2.2.0 + +Dependency updates only. + ## 2.1.0 Dependency updates only. diff --git a/examples/data-objects/canvas/package.json b/examples/data-objects/canvas/package.json index e079d6014c23..b21e8d248bbb 100644 --- a/examples/data-objects/canvas/package.json +++ b/examples/data-objects/canvas/package.json @@ -1,6 +1,6 @@ { "name": "@fluid-example/canvas", - "version": "2.2.0", + "version": "2.3.0", "private": true, "description": "Fluid ink canvas", "homepage": "https://fluidframework.com", @@ -51,7 +51,7 @@ "@biomejs/biome": "~1.8.3", "@fluid-example/webpack-fluid-loader": "workspace:~", "@fluidframework/build-common": "^2.0.3", - "@fluidframework/build-tools": "0.43.0-285387", + "@fluidframework/build-tools": "^0.44.0", "@fluidframework/eslint-config-fluid": "^5.3.0", "@fluidframework/test-tools": "^1.0.195075", "@types/jest": "29.5.3", diff --git a/examples/data-objects/clicker/CHANGELOG.md b/examples/data-objects/clicker/CHANGELOG.md index 492139744ce9..76b33c26de90 100644 --- a/examples/data-objects/clicker/CHANGELOG.md +++ b/examples/data-objects/clicker/CHANGELOG.md @@ -1,5 +1,9 @@ # @fluid-example/clicker +## 2.2.0 + +Dependency updates only. + ## 2.1.0 Dependency updates only. diff --git a/examples/data-objects/clicker/package.json b/examples/data-objects/clicker/package.json index a07d47163505..41d2ff2b27e9 100644 --- a/examples/data-objects/clicker/package.json +++ b/examples/data-objects/clicker/package.json @@ -1,6 +1,6 @@ { "name": "@fluid-example/clicker", - "version": "2.2.0", + "version": "2.3.0", "private": true, "description": "Minimal Fluid component sample to implement a collaborative counter.", "homepage": "https://fluidframework.com", @@ -63,7 +63,7 @@ "@biomejs/biome": "~1.8.3", "@fluid-example/webpack-fluid-loader": "workspace:~", "@fluidframework/build-common": "^2.0.3", - "@fluidframework/build-tools": "0.43.0-285387", + "@fluidframework/build-tools": "^0.44.0", "@fluidframework/eslint-config-fluid": "^5.3.0", "@fluidframework/test-tools": "^1.0.195075", "@fluidframework/test-utils": "workspace:~", diff --git a/examples/data-objects/codemirror/CHANGELOG.md b/examples/data-objects/codemirror/CHANGELOG.md index 6b45901d3810..10964542a6e7 100644 --- a/examples/data-objects/codemirror/CHANGELOG.md +++ b/examples/data-objects/codemirror/CHANGELOG.md @@ -1,5 +1,9 @@ # @fluid-example/codemirror +## 2.2.0 + +Dependency updates only. + ## 2.1.0 Dependency updates only. diff --git a/examples/data-objects/codemirror/package.json b/examples/data-objects/codemirror/package.json index 45218e77b5f7..9bafad0f6dd9 100644 --- a/examples/data-objects/codemirror/package.json +++ b/examples/data-objects/codemirror/package.json @@ -1,6 +1,6 @@ { "name": "@fluid-example/codemirror", - "version": "2.2.0", + "version": "2.3.0", "private": true, "description": "Simple markdown editor", "homepage": "https://fluidframework.com", @@ -70,7 +70,7 @@ "@biomejs/biome": "~1.8.3", "@fluid-example/webpack-fluid-loader": "workspace:~", "@fluidframework/build-common": "^2.0.3", - "@fluidframework/build-tools": "0.43.0-285387", + "@fluidframework/build-tools": "^0.44.0", "@fluidframework/eslint-config-fluid": "^5.3.0", "@types/codemirror": "5.60.7", "@types/node": "^18.19.0", diff --git a/examples/data-objects/diceroller/CHANGELOG.md b/examples/data-objects/diceroller/CHANGELOG.md index dd797d2d75ac..4f96832fb69b 100644 --- a/examples/data-objects/diceroller/CHANGELOG.md +++ b/examples/data-objects/diceroller/CHANGELOG.md @@ -1,5 +1,9 @@ # @fluid-example/diceroller +## 2.2.0 + +Dependency updates only. + ## 2.1.0 Dependency updates only. diff --git a/examples/data-objects/diceroller/package.json b/examples/data-objects/diceroller/package.json index 3c70a58d6276..a80ae5646074 100644 --- a/examples/data-objects/diceroller/package.json +++ b/examples/data-objects/diceroller/package.json @@ -1,6 +1,6 @@ { "name": "@fluid-example/diceroller", - "version": "2.2.0", + "version": "2.3.0", "private": true, "description": "Minimal Fluid Container & Object sample to implement a collaborative dice roller.", "homepage": "https://fluidframework.com", @@ -50,7 +50,7 @@ "@biomejs/biome": "~1.8.3", "@fluid-example/webpack-fluid-loader": "workspace:~", "@fluidframework/build-common": "^2.0.3", - "@fluidframework/build-tools": "0.43.0-285387", + "@fluidframework/build-tools": "^0.44.0", "@fluidframework/eslint-config-fluid": "^5.3.0", "@fluidframework/test-tools": "^1.0.195075", "@types/jest": "29.5.3", diff --git a/examples/data-objects/inventory-app/CHANGELOG.md b/examples/data-objects/inventory-app/CHANGELOG.md index 96ce2a7d9f7c..db634ee34ca9 100644 --- a/examples/data-objects/inventory-app/CHANGELOG.md +++ b/examples/data-objects/inventory-app/CHANGELOG.md @@ -1,5 +1,9 @@ # @fluid-experimental/inventory-app +## 2.2.0 + +Dependency updates only. + ## 2.1.0 Dependency updates only. diff --git a/examples/data-objects/inventory-app/package.json b/examples/data-objects/inventory-app/package.json index a0790bce2546..1b648b83c6c1 100644 --- a/examples/data-objects/inventory-app/package.json +++ b/examples/data-objects/inventory-app/package.json @@ -1,6 +1,6 @@ { "name": "@fluid-example/inventory-app", - "version": "2.2.0", + "version": "2.3.0", "private": true, "description": "Minimal sample of SharedTree/React integration.", "homepage": "https://fluidframework.com", @@ -54,7 +54,7 @@ "@biomejs/biome": "~1.8.3", "@fluid-example/webpack-fluid-loader": "workspace:~", "@fluidframework/build-common": "^2.0.3", - "@fluidframework/build-tools": "0.43.0-285387", + "@fluidframework/build-tools": "^0.44.0", "@fluidframework/eslint-config-fluid": "^5.3.0", "@fluidframework/test-tools": "^1.0.195075", "@types/jest": "29.5.3", diff --git a/examples/data-objects/monaco/CHANGELOG.md b/examples/data-objects/monaco/CHANGELOG.md index fc7779829f3e..d20fca4d61a5 100644 --- a/examples/data-objects/monaco/CHANGELOG.md +++ b/examples/data-objects/monaco/CHANGELOG.md @@ -1,5 +1,9 @@ # @fluid-example/monaco +## 2.2.0 + +Dependency updates only. + ## 2.1.0 Dependency updates only. diff --git a/examples/data-objects/monaco/package.json b/examples/data-objects/monaco/package.json index 4012fe7dbdd2..16d67505e582 100644 --- a/examples/data-objects/monaco/package.json +++ b/examples/data-objects/monaco/package.json @@ -1,6 +1,6 @@ { "name": "@fluid-example/monaco", - "version": "2.2.0", + "version": "2.3.0", "private": true, "description": "Monaco code editor", "homepage": "https://fluidframework.com", @@ -52,7 +52,7 @@ "@biomejs/biome": "~1.8.3", "@fluid-example/webpack-fluid-loader": "workspace:~", "@fluidframework/build-common": "^2.0.3", - "@fluidframework/build-tools": "0.43.0-285387", + "@fluidframework/build-tools": "^0.44.0", "@fluidframework/eslint-config-fluid": "^5.3.0", "@types/react": "^17.0.44", "css-loader": "^6.11.0", diff --git a/examples/data-objects/multiview/constellation-model/CHANGELOG.md b/examples/data-objects/multiview/constellation-model/CHANGELOG.md index b1e792a9df34..5b111e4df2fd 100644 --- a/examples/data-objects/multiview/constellation-model/CHANGELOG.md +++ b/examples/data-objects/multiview/constellation-model/CHANGELOG.md @@ -1,5 +1,9 @@ # @fluid-example/multiview-constellation-model +## 2.2.0 + +Dependency updates only. + ## 2.1.0 Dependency updates only. diff --git a/examples/data-objects/multiview/constellation-model/package.json b/examples/data-objects/multiview/constellation-model/package.json index 32fff337d20d..64d3c14f0969 100644 --- a/examples/data-objects/multiview/constellation-model/package.json +++ b/examples/data-objects/multiview/constellation-model/package.json @@ -1,6 +1,6 @@ { "name": "@fluid-example/multiview-constellation-model", - "version": "2.2.0", + "version": "2.3.0", "private": true, "description": "Constellation model for multiview sample", "homepage": "https://fluidframework.com", @@ -48,7 +48,7 @@ "devDependencies": { "@biomejs/biome": "~1.8.3", "@fluidframework/build-common": "^2.0.3", - "@fluidframework/build-tools": "0.43.0-285387", + "@fluidframework/build-tools": "^0.44.0", "@fluidframework/eslint-config-fluid": "^5.3.0", "eslint": "~8.55.0", "prettier": "~3.0.3", diff --git a/examples/data-objects/multiview/constellation-view/CHANGELOG.md b/examples/data-objects/multiview/constellation-view/CHANGELOG.md index 86d36911ec16..56c4c8b18175 100644 --- a/examples/data-objects/multiview/constellation-view/CHANGELOG.md +++ b/examples/data-objects/multiview/constellation-view/CHANGELOG.md @@ -1,5 +1,9 @@ # @fluid-example/multiview-constellation-view +## 2.2.0 + +Dependency updates only. + ## 2.1.0 Dependency updates only. diff --git a/examples/data-objects/multiview/constellation-view/package.json b/examples/data-objects/multiview/constellation-view/package.json index 124a1549df45..b55f0d70401c 100644 --- a/examples/data-objects/multiview/constellation-view/package.json +++ b/examples/data-objects/multiview/constellation-view/package.json @@ -1,6 +1,6 @@ { "name": "@fluid-example/multiview-constellation-view", - "version": "2.2.0", + "version": "2.3.0", "private": true, "description": "View for multiview sample", "homepage": "https://fluidframework.com", @@ -47,7 +47,7 @@ "devDependencies": { "@biomejs/biome": "~1.8.3", "@fluidframework/build-common": "^2.0.3", - "@fluidframework/build-tools": "0.43.0-285387", + "@fluidframework/build-tools": "^0.44.0", "@fluidframework/eslint-config-fluid": "^5.3.0", "@types/react": "^17.0.44", "copyfiles": "^2.4.1", diff --git a/examples/data-objects/multiview/container/CHANGELOG.md b/examples/data-objects/multiview/container/CHANGELOG.md index 61e571a6dc41..5d78dfae37e2 100644 --- a/examples/data-objects/multiview/container/CHANGELOG.md +++ b/examples/data-objects/multiview/container/CHANGELOG.md @@ -1,5 +1,9 @@ # @fluid-example/multiview-container +## 2.2.0 + +Dependency updates only. + ## 2.1.0 Dependency updates only. diff --git a/examples/data-objects/multiview/container/package.json b/examples/data-objects/multiview/container/package.json index 133918fbc5a6..98d7613e8010 100644 --- a/examples/data-objects/multiview/container/package.json +++ b/examples/data-objects/multiview/container/package.json @@ -1,6 +1,6 @@ { "name": "@fluid-example/multiview-container", - "version": "2.2.0", + "version": "2.3.0", "private": true, "description": "Container for multiview sample", "homepage": "https://fluidframework.com", @@ -62,7 +62,7 @@ "@biomejs/biome": "~1.8.3", "@fluid-example/webpack-fluid-loader": "workspace:~", "@fluidframework/build-common": "^2.0.3", - "@fluidframework/build-tools": "0.43.0-285387", + "@fluidframework/build-tools": "^0.44.0", "@fluidframework/eslint-config-fluid": "^5.3.0", "@fluidframework/test-tools": "^1.0.195075", "@types/jest": "29.5.3", diff --git a/examples/data-objects/multiview/coordinate-model/CHANGELOG.md b/examples/data-objects/multiview/coordinate-model/CHANGELOG.md index 5a38743e418c..d5c8a557410e 100644 --- a/examples/data-objects/multiview/coordinate-model/CHANGELOG.md +++ b/examples/data-objects/multiview/coordinate-model/CHANGELOG.md @@ -1,5 +1,9 @@ # @fluid-example/multiview-coordinate-model +## 2.2.0 + +Dependency updates only. + ## 2.1.0 Dependency updates only. diff --git a/examples/data-objects/multiview/coordinate-model/package.json b/examples/data-objects/multiview/coordinate-model/package.json index db5fc861a354..852cf6e4b44e 100644 --- a/examples/data-objects/multiview/coordinate-model/package.json +++ b/examples/data-objects/multiview/coordinate-model/package.json @@ -1,6 +1,6 @@ { "name": "@fluid-example/multiview-coordinate-model", - "version": "2.2.0", + "version": "2.3.0", "private": true, "description": "Coordinate model for multiview sample", "homepage": "https://fluidframework.com", @@ -46,7 +46,7 @@ "devDependencies": { "@biomejs/biome": "~1.8.3", "@fluidframework/build-common": "^2.0.3", - "@fluidframework/build-tools": "0.43.0-285387", + "@fluidframework/build-tools": "^0.44.0", "@fluidframework/eslint-config-fluid": "^5.3.0", "eslint": "~8.55.0", "prettier": "~3.0.3", diff --git a/examples/data-objects/multiview/interface/CHANGELOG.md b/examples/data-objects/multiview/interface/CHANGELOG.md index c2ee0200aef5..efb23ffc0394 100644 --- a/examples/data-objects/multiview/interface/CHANGELOG.md +++ b/examples/data-objects/multiview/interface/CHANGELOG.md @@ -1,5 +1,9 @@ # @fluid-example/multiview-coordinate-interface +## 2.2.0 + +Dependency updates only. + ## 2.1.0 Dependency updates only. diff --git a/examples/data-objects/multiview/interface/package.json b/examples/data-objects/multiview/interface/package.json index 9658789f01f9..625307f02f2c 100644 --- a/examples/data-objects/multiview/interface/package.json +++ b/examples/data-objects/multiview/interface/package.json @@ -1,6 +1,6 @@ { "name": "@fluid-example/multiview-coordinate-interface", - "version": "2.2.0", + "version": "2.3.0", "private": true, "description": "Interface for multiview sample", "homepage": "https://fluidframework.com", @@ -43,9 +43,9 @@ }, "devDependencies": { "@biomejs/biome": "~1.8.3", - "@fluid-tools/build-cli": "0.43.0-285387", + "@fluid-tools/build-cli": "^0.44.0", "@fluidframework/build-common": "^2.0.3", - "@fluidframework/build-tools": "0.43.0-285387", + "@fluidframework/build-tools": "^0.44.0", "@fluidframework/eslint-config-fluid": "^5.3.0", "@types/react": "^17.0.44", "eslint": "~8.55.0", diff --git a/examples/data-objects/multiview/plot-coordinate-view/CHANGELOG.md b/examples/data-objects/multiview/plot-coordinate-view/CHANGELOG.md index f21e6788b63b..dd0348a743a6 100644 --- a/examples/data-objects/multiview/plot-coordinate-view/CHANGELOG.md +++ b/examples/data-objects/multiview/plot-coordinate-view/CHANGELOG.md @@ -1,5 +1,9 @@ # @fluid-example/multiview-plot-coordinate-view +## 2.2.0 + +Dependency updates only. + ## 2.1.0 Dependency updates only. diff --git a/examples/data-objects/multiview/plot-coordinate-view/package.json b/examples/data-objects/multiview/plot-coordinate-view/package.json index 9cb6284a6bf9..6e02cb3be195 100644 --- a/examples/data-objects/multiview/plot-coordinate-view/package.json +++ b/examples/data-objects/multiview/plot-coordinate-view/package.json @@ -1,6 +1,6 @@ { "name": "@fluid-example/multiview-plot-coordinate-view", - "version": "2.2.0", + "version": "2.3.0", "private": true, "description": "View for multiview sample", "homepage": "https://fluidframework.com", @@ -46,7 +46,7 @@ "devDependencies": { "@biomejs/biome": "~1.8.3", "@fluidframework/build-common": "^2.0.3", - "@fluidframework/build-tools": "0.43.0-285387", + "@fluidframework/build-tools": "^0.44.0", "@fluidframework/eslint-config-fluid": "^5.3.0", "@types/react": "^17.0.44", "copyfiles": "^2.4.1", diff --git a/examples/data-objects/multiview/slider-coordinate-view/CHANGELOG.md b/examples/data-objects/multiview/slider-coordinate-view/CHANGELOG.md index 603f1462805d..d6d10776f10e 100644 --- a/examples/data-objects/multiview/slider-coordinate-view/CHANGELOG.md +++ b/examples/data-objects/multiview/slider-coordinate-view/CHANGELOG.md @@ -1,5 +1,9 @@ # @fluid-example/multiview-slider-coordinate-view +## 2.2.0 + +Dependency updates only. + ## 2.1.0 Dependency updates only. diff --git a/examples/data-objects/multiview/slider-coordinate-view/package.json b/examples/data-objects/multiview/slider-coordinate-view/package.json index 655e406f7ecc..ef7a5aea47b9 100644 --- a/examples/data-objects/multiview/slider-coordinate-view/package.json +++ b/examples/data-objects/multiview/slider-coordinate-view/package.json @@ -1,6 +1,6 @@ { "name": "@fluid-example/multiview-slider-coordinate-view", - "version": "2.2.0", + "version": "2.3.0", "private": true, "description": "View for multiview sample", "homepage": "https://fluidframework.com", @@ -46,7 +46,7 @@ "devDependencies": { "@biomejs/biome": "~1.8.3", "@fluidframework/build-common": "^2.0.3", - "@fluidframework/build-tools": "0.43.0-285387", + "@fluidframework/build-tools": "^0.44.0", "@fluidframework/eslint-config-fluid": "^5.3.0", "@types/react": "^17.0.44", "copyfiles": "^2.4.1", diff --git a/examples/data-objects/multiview/triangle-view/CHANGELOG.md b/examples/data-objects/multiview/triangle-view/CHANGELOG.md index 5dd01426ee31..878f45b65761 100644 --- a/examples/data-objects/multiview/triangle-view/CHANGELOG.md +++ b/examples/data-objects/multiview/triangle-view/CHANGELOG.md @@ -1,5 +1,9 @@ # @fluid-example/multiview-triangle-view +## 2.2.0 + +Dependency updates only. + ## 2.1.0 Dependency updates only. diff --git a/examples/data-objects/multiview/triangle-view/package.json b/examples/data-objects/multiview/triangle-view/package.json index 254074cafcd1..ab216ae0b9cb 100644 --- a/examples/data-objects/multiview/triangle-view/package.json +++ b/examples/data-objects/multiview/triangle-view/package.json @@ -1,6 +1,6 @@ { "name": "@fluid-example/multiview-triangle-view", - "version": "2.2.0", + "version": "2.3.0", "private": true, "description": "View for multiview sample", "homepage": "https://fluidframework.com", @@ -46,7 +46,7 @@ "devDependencies": { "@biomejs/biome": "~1.8.3", "@fluidframework/build-common": "^2.0.3", - "@fluidframework/build-tools": "0.43.0-285387", + "@fluidframework/build-tools": "^0.44.0", "@fluidframework/eslint-config-fluid": "^5.3.0", "@types/react": "^17.0.44", "copyfiles": "^2.4.1", diff --git a/examples/data-objects/prosemirror/CHANGELOG.md b/examples/data-objects/prosemirror/CHANGELOG.md index 8e93f86b36a6..a2e6d85afd49 100644 --- a/examples/data-objects/prosemirror/CHANGELOG.md +++ b/examples/data-objects/prosemirror/CHANGELOG.md @@ -1,5 +1,9 @@ # @fluid-example/prosemirror +## 2.2.0 + +Dependency updates only. + ## 2.1.0 Dependency updates only. diff --git a/examples/data-objects/prosemirror/package.json b/examples/data-objects/prosemirror/package.json index 83e97a61e525..834d4bb13a81 100644 --- a/examples/data-objects/prosemirror/package.json +++ b/examples/data-objects/prosemirror/package.json @@ -1,6 +1,6 @@ { "name": "@fluid-example/prosemirror", - "version": "2.2.0", + "version": "2.3.0", "private": true, "description": "ProseMirror", "homepage": "https://fluidframework.com", @@ -81,7 +81,7 @@ "@biomejs/biome": "~1.8.3", "@fluid-example/webpack-fluid-loader": "workspace:~", "@fluidframework/build-common": "^2.0.3", - "@fluidframework/build-tools": "0.43.0-285387", + "@fluidframework/build-tools": "^0.44.0", "@fluidframework/eslint-config-fluid": "^5.3.0", "@types/node": "^18.19.0", "@types/orderedmap": "^1.0.0", diff --git a/examples/data-objects/prosemirror/src/fluidBridge.ts b/examples/data-objects/prosemirror/src/fluidBridge.ts index 5be4a7025c44..c2cef3d66472 100644 --- a/examples/data-objects/prosemirror/src/fluidBridge.ts +++ b/examples/data-objects/prosemirror/src/fluidBridge.ts @@ -450,10 +450,7 @@ function sliceToGroupOpsInternal( const node = schema.nodes[value.type]; if (node.isInline) { if (value.type === "text") { - const segment = new TextSegment(value.text); - if (props) { - segment.addProperties(props); - } + const segment = TextSegment.make(value.text, props); ops.push(createInsertSegmentOp(from + offset, segment)); offset = adjustOffset(from, offset, value.text.length, insert, gapDistance); @@ -466,8 +463,7 @@ function sliceToGroupOpsInternal( }, }; - const marker = new Marker(ReferenceType.Simple); - marker.addProperties(nodeProps); + const marker = Marker.make(ReferenceType.Simple, nodeProps); ops.push(createInsertSegmentOp(from + offset, marker)); offset = adjustOffset(from, offset, 1, insert, gapDistance); @@ -483,8 +479,7 @@ function sliceToGroupOpsInternal( }, }; - const marker = new Marker(ReferenceType.Simple); - marker.addProperties(beginProps); + const marker = Marker.make(ReferenceType.Simple, beginProps); ops.push(createInsertSegmentOp(from + offset, marker)); offset = adjustOffset(from, offset, 1, insert, gapDistance); @@ -514,8 +509,7 @@ function sliceToGroupOpsInternal( }, }; - const marker = new Marker(ReferenceType.Simple); - marker.addProperties(endProps); + const marker = Marker.make(ReferenceType.Simple, endProps); ops.push(createInsertSegmentOp(from + offset, marker)); offset = adjustOffset(from, offset, 1, insert, gapDistance); diff --git a/examples/data-objects/smde/CHANGELOG.md b/examples/data-objects/smde/CHANGELOG.md index 2bc596e29a14..f0cac001a6ec 100644 --- a/examples/data-objects/smde/CHANGELOG.md +++ b/examples/data-objects/smde/CHANGELOG.md @@ -1,5 +1,9 @@ # @fluid-example/smde +## 2.2.0 + +Dependency updates only. + ## 2.1.0 Dependency updates only. diff --git a/examples/data-objects/smde/package.json b/examples/data-objects/smde/package.json index 62d57a69d29c..4699a05b736d 100644 --- a/examples/data-objects/smde/package.json +++ b/examples/data-objects/smde/package.json @@ -1,6 +1,6 @@ { "name": "@fluid-example/smde", - "version": "2.2.0", + "version": "2.3.0", "private": true, "description": "Simple markdown editor", "homepage": "https://fluidframework.com", @@ -60,7 +60,7 @@ "@biomejs/biome": "~1.8.3", "@fluid-example/webpack-fluid-loader": "workspace:~", "@fluidframework/build-common": "^2.0.3", - "@fluidframework/build-tools": "0.43.0-285387", + "@fluidframework/build-tools": "^0.44.0", "@fluidframework/eslint-config-fluid": "^5.3.0", "@types/react": "^17.0.44", "@types/simplemde": "^1.11.7", diff --git a/examples/data-objects/table-document/CHANGELOG.md b/examples/data-objects/table-document/CHANGELOG.md index 2338905089cf..da8118cd5404 100644 --- a/examples/data-objects/table-document/CHANGELOG.md +++ b/examples/data-objects/table-document/CHANGELOG.md @@ -1,5 +1,9 @@ # @fluid-example/table-document +## 2.2.0 + +Dependency updates only. + ## 2.1.0 Dependency updates only. diff --git a/examples/data-objects/table-document/package.json b/examples/data-objects/table-document/package.json index c2bfb2152a65..82c076886c04 100644 --- a/examples/data-objects/table-document/package.json +++ b/examples/data-objects/table-document/package.json @@ -1,6 +1,6 @@ { "name": "@fluid-example/table-document", - "version": "2.2.0", + "version": "2.3.0", "description": "Chaincode component containing a table's data", "homepage": "https://fluidframework.com", "repository": { @@ -93,9 +93,9 @@ "@biomejs/biome": "~1.8.3", "@fluid-internal/mocha-test-setup": "workspace:~", "@fluid-private/test-version-utils": "workspace:~", - "@fluid-tools/build-cli": "0.43.0-285387", + "@fluid-tools/build-cli": "^0.44.0", "@fluidframework/build-common": "^2.0.3", - "@fluidframework/build-tools": "0.43.0-285387", + "@fluidframework/build-tools": "^0.44.0", "@fluidframework/eslint-config-fluid": "^5.3.0", "@fluidframework/runtime-utils": "workspace:~", "@fluidframework/test-utils": "workspace:~", diff --git a/examples/data-objects/todo/CHANGELOG.md b/examples/data-objects/todo/CHANGELOG.md index 7e5fcd08648d..cd39b1c8550a 100644 --- a/examples/data-objects/todo/CHANGELOG.md +++ b/examples/data-objects/todo/CHANGELOG.md @@ -1,5 +1,9 @@ # @fluid-example/todo +## 2.2.0 + +Dependency updates only. + ## 2.1.0 Dependency updates only. diff --git a/examples/data-objects/todo/package.json b/examples/data-objects/todo/package.json index 6c42d85d397c..0de3c892f664 100644 --- a/examples/data-objects/todo/package.json +++ b/examples/data-objects/todo/package.json @@ -1,6 +1,6 @@ { "name": "@fluid-example/todo", - "version": "2.2.0", + "version": "2.3.0", "private": true, "description": "Simple todo canvas.", "homepage": "https://fluidframework.com", @@ -56,7 +56,7 @@ "@biomejs/biome": "~1.8.3", "@fluid-example/webpack-fluid-loader": "workspace:~", "@fluidframework/build-common": "^2.0.3", - "@fluidframework/build-tools": "0.43.0-285387", + "@fluidframework/build-tools": "^0.44.0", "@fluidframework/eslint-config-fluid": "^5.3.0", "@fluidframework/test-tools": "^1.0.195075", "@fluidframework/test-utils": "workspace:~", diff --git a/examples/data-objects/webflow/CHANGELOG.md b/examples/data-objects/webflow/CHANGELOG.md index bb86d5b62108..e72ec36c991b 100644 --- a/examples/data-objects/webflow/CHANGELOG.md +++ b/examples/data-objects/webflow/CHANGELOG.md @@ -1,5 +1,9 @@ # @fluid-example/webflow +## 2.2.0 + +Dependency updates only. + ## 2.1.0 Dependency updates only. diff --git a/examples/data-objects/webflow/package.json b/examples/data-objects/webflow/package.json index 90a9848b0c67..aadb05353744 100644 --- a/examples/data-objects/webflow/package.json +++ b/examples/data-objects/webflow/package.json @@ -1,6 +1,6 @@ { "name": "@fluid-example/webflow", - "version": "2.2.0", + "version": "2.3.0", "private": true, "description": "Collaborative markdown editor.", "homepage": "https://fluidframework.com", @@ -93,7 +93,7 @@ "@fluid-internal/mocha-test-setup": "workspace:~", "@fluid-private/test-version-utils": "workspace:~", "@fluidframework/build-common": "^2.0.3", - "@fluidframework/build-tools": "0.43.0-285387", + "@fluidframework/build-tools": "^0.44.0", "@fluidframework/eslint-config-fluid": "^5.3.0", "@fluidframework/runtime-utils": "workspace:~", "@fluidframework/test-utils": "workspace:~", diff --git a/examples/external-data/CHANGELOG.md b/examples/external-data/CHANGELOG.md index c8f9e9c520c6..000e36fce91b 100644 --- a/examples/external-data/CHANGELOG.md +++ b/examples/external-data/CHANGELOG.md @@ -1,5 +1,9 @@ # @fluid-example/app-integration-external-data +## 2.2.0 + +Dependency updates only. + ## 2.1.0 Dependency updates only. diff --git a/examples/external-data/package.json b/examples/external-data/package.json index 685186cd1082..587fd0985e93 100644 --- a/examples/external-data/package.json +++ b/examples/external-data/package.json @@ -1,6 +1,6 @@ { "name": "@fluid-example/app-integration-external-data", - "version": "2.2.0", + "version": "2.3.0", "private": true, "description": "Integrating an external data source with Fluid data.", "homepage": "https://fluidframework.com", @@ -83,7 +83,7 @@ "devDependencies": { "@biomejs/biome": "~1.8.3", "@fluidframework/build-common": "^2.0.3", - "@fluidframework/build-tools": "0.43.0-285387", + "@fluidframework/build-tools": "^0.44.0", "@fluidframework/eslint-config-fluid": "^5.3.0", "@fluidframework/test-tools": "^1.0.195075", "@types/cors": "^2.8.4", diff --git a/examples/service-clients/azure-client/external-controller/CHANGELOG.md b/examples/service-clients/azure-client/external-controller/CHANGELOG.md index bfdc8ed685e2..9f59044397d1 100644 --- a/examples/service-clients/azure-client/external-controller/CHANGELOG.md +++ b/examples/service-clients/azure-client/external-controller/CHANGELOG.md @@ -1,5 +1,9 @@ # @fluid-example/app-integration-external-controller +## 2.2.0 + +Dependency updates only. + ## 2.1.0 Dependency updates only. diff --git a/examples/service-clients/azure-client/external-controller/package.json b/examples/service-clients/azure-client/external-controller/package.json index 7fb628c082a1..91b3bfc0b9d5 100644 --- a/examples/service-clients/azure-client/external-controller/package.json +++ b/examples/service-clients/azure-client/external-controller/package.json @@ -1,6 +1,6 @@ { "name": "@fluid-example/app-integration-external-controller", - "version": "2.2.0", + "version": "2.3.0", "private": true, "description": "Minimal Fluid Container & Data Object sample to implement a collaborative dice roller as a standalone app.", "homepage": "https://fluidframework.com", @@ -55,7 +55,7 @@ "devDependencies": { "@biomejs/biome": "~1.8.3", "@fluidframework/build-common": "^2.0.3", - "@fluidframework/build-tools": "0.43.0-285387", + "@fluidframework/build-tools": "^0.44.0", "@fluidframework/container-definitions": "workspace:~", "@fluidframework/container-loader": "workspace:~", "@fluidframework/devtools": "workspace:~", diff --git a/examples/service-clients/azure-client/external-controller/src/app.ts b/examples/service-clients/azure-client/external-controller/src/app.ts index beea74eafe2e..f42de8013719 100644 --- a/examples/service-clients/azure-client/external-controller/src/app.ts +++ b/examples/service-clients/azure-client/external-controller/src/app.ts @@ -13,6 +13,7 @@ import { createDevtoolsLogger, initializeDevtools } from "@fluidframework/devtoo import { ISharedMap, IValueChanged, SharedMap } from "@fluidframework/map/internal"; import { createChildLogger } from "@fluidframework/telemetry-utils/internal"; import { InsecureTokenProvider } from "@fluidframework/test-runtime-utils/internal"; +import type { ContainerSchema } from "fluid-framework"; import { IFluidContainer } from "fluid-framework"; import { v4 as uuid } from "uuid"; @@ -66,7 +67,8 @@ const containerSchema = { map1: SharedMap, map2: SharedMap, }, -}; +} satisfies ContainerSchema; +type DiceRollerContainerSchema = typeof containerSchema; function createDiceRollerControllerProps(map: ISharedMap): DiceRollerControllerProps { return { @@ -90,17 +92,17 @@ function createDiceRollerControllerProps(map: ISharedMap): DiceRollerControllerP } function createDiceRollerControllerPropsFromContainer( - container: IFluidContainer, + container: IFluidContainer, ): [DiceRollerControllerProps, DiceRollerControllerProps] { const diceRollerController1Props: DiceRollerControllerProps = - createDiceRollerControllerProps(container.initialObjects.map1 as ISharedMap); + createDiceRollerControllerProps(container.initialObjects.map1); const diceRollerController2Props: DiceRollerControllerProps = - createDiceRollerControllerProps(container.initialObjects.map2 as ISharedMap); + createDiceRollerControllerProps(container.initialObjects.map2); return [diceRollerController1Props, diceRollerController2Props]; } async function initializeNewContainer( - container: IFluidContainer, + container: IFluidContainer, ): Promise<[DiceRollerControllerProps, DiceRollerControllerProps]> { const [diceRollerController1Props, diceRollerController2Props] = createDiceRollerControllerPropsFromContainer(container); @@ -127,7 +129,7 @@ async function start(): Promise { logger: devtoolsLogger, }; const client = new AzureClient(clientProps); - let container: IFluidContainer; + let container: IFluidContainer; let services: AzureContainerServices; let id: string; diff --git a/examples/service-clients/azure-client/external-controller/tests/index.ts b/examples/service-clients/azure-client/external-controller/tests/index.ts index 3de14b188a73..05cb4c99117d 100644 --- a/examples/service-clients/azure-client/external-controller/tests/index.ts +++ b/examples/service-clients/azure-client/external-controller/tests/index.ts @@ -5,7 +5,13 @@ /* eslint-disable import/no-internal-modules */ -import { type ISharedMap, SharedMap } from "@fluidframework/map/internal"; +import { + IContainer, + IFluidModuleWithDetails, + IRuntimeFactory, +} from "@fluidframework/container-definitions/internal"; +import { Loader } from "@fluidframework/container-loader/internal"; +import { createDOProviderContainerRuntimeFactory } from "@fluidframework/fluid-static/internal"; import { LocalDocumentServiceFactory, LocalResolver, @@ -15,26 +21,11 @@ import { ILocalDeltaConnectionServer, LocalDeltaConnectionServer, } from "@fluidframework/server-local-server"; +import type { IFluidContainer, ContainerSchema } from "fluid-framework"; +import { SharedMap } from "fluid-framework/legacy"; -import { IFluidContainer } from "@fluidframework/fluid-static"; -import { createDOProviderContainerRuntimeFactory } from "@fluidframework/fluid-static/internal"; import { DiceRollerController } from "../src/controller.js"; import { makeAppView } from "../src/view.js"; -import { - IContainer, - IFluidModuleWithDetails, - IRuntimeFactory, -} from "@fluidframework/container-definitions/internal"; -import { Loader } from "@fluidframework/container-loader/internal"; - -// Since this is a single page Fluid application we are generating a new document id -// if one was not provided -let createNew = false; -if (window.location.hash.length === 0) { - createNew = true; - window.location.hash = Date.now().toString(); -} -const documentId = window.location.hash.substring(1); // The local server needs to be shared across the Loader instances for collaboration to happen const localServerMap = new Map(); @@ -43,23 +34,23 @@ const urlResolver = new LocalResolver(); /** * Connect to the local SessionStorage Fluid service and retrieve a Container with the given ID running the given code. - * @param documentId - The document id to retrieve or create + * @param containerId - The document id to retrieve or create * @param containerRuntimeFactory - The container factory to be loaded in the container * @internal */ export async function getSessionStorageContainer( - documentId: string, + containerId: string, containerRuntimeFactory: IRuntimeFactory, createNew: boolean, ): Promise<{ container: IContainer; attach: (() => Promise) | undefined }> { - let localServer = localServerMap.get(documentId); + let localServer = localServerMap.get(containerId); if (localServer === undefined) { localServer = LocalDeltaConnectionServer.create(new LocalSessionStorageDbFactory()); - localServerMap.set(documentId, localServer); + localServerMap.set(containerId, localServer); } const documentServiceFactory = new LocalDocumentServiceFactory(localServer); - const url = `${window.location.origin}/${documentId}`; + const url = `${window.location.origin}/${containerId}`; // To bypass proposal-based loading, we need a codeLoader that will return our already-in-memory container factory. // The expected format of that response is an IFluidModule with a fluidExport. @@ -86,7 +77,7 @@ export async function getSessionStorageContainer( // proposal), but the IContainer will only give us a NullRuntime if there's no proposal. So we'll use a fake // proposal. container = await loader.createDetachedContainer({ package: "", config: {} }); - attach = async () => container.attach({ url }); + attach = async (): Promise => container.attach({ url }); } else { container = await loader.resolve({ url }); } @@ -94,22 +85,22 @@ export async function getSessionStorageContainer( return { container, attach }; } -/** - * @internal - */ -export const containerConfig = { +const containerConfig = { name: "dice-roller-container", initialObjects: { /* [id]: DataObject */ map1: SharedMap, map2: SharedMap, }, -}; +} satisfies ContainerSchema & { name: string }; +type TestContainerSchema = typeof containerConfig; -async function initializeNewContainer(container: IFluidContainer): Promise { +async function initializeNewContainer( + container: IFluidContainer, +): Promise { // We now get the first SharedMap from the container - const sharedMap1 = container.initialObjects.map1 as ISharedMap; - const sharedMap2 = container.initialObjects.map2 as ISharedMap; + const sharedMap1 = container.initialObjects.map1; + const sharedMap2 = container.initialObjects.map2; await Promise.all([ DiceRollerController.initializeModel(sharedMap1), DiceRollerController.initializeModel(sharedMap2), @@ -119,16 +110,16 @@ async function initializeNewContainer(container: IFluidContainer): Promise /** * This is a helper function for loading the page. It's required because getting the Fluid Container * requires making async calls. - * @internal */ -export async function createContainerAndRenderInElement( +async function createContainerAndRenderInElement( + containerId: string, element: HTMLDivElement, createNewFlag: boolean, -) { +): Promise { // The SessionStorage Container is an in-memory Fluid container that uses the local browser SessionStorage // to store ops. const { container, attach } = await getSessionStorageContainer( - documentId, + containerId, createDOProviderContainerRuntimeFactory({ schema: containerConfig, compatibilityMode: "2", @@ -137,14 +128,15 @@ export async function createContainerAndRenderInElement( ); // Get the Default Object from the Container - const fluidContainer = (await container.getEntryPoint()) as IFluidContainer; + const fluidContainer = + (await container.getEntryPoint()) as IFluidContainer; if (createNewFlag) { await initializeNewContainer(fluidContainer); await attach?.(); } - const sharedMap1 = fluidContainer.initialObjects.map1 as ISharedMap; - const sharedMap2 = fluidContainer.initialObjects.map2 as ISharedMap; + const sharedMap1 = fluidContainer.initialObjects.map1; + const sharedMap2 = fluidContainer.initialObjects.map2; const diceRollerController = new DiceRollerController(sharedMap1); const diceRollerController2 = new DiceRollerController(sharedMap2); @@ -154,28 +146,38 @@ export async function createContainerAndRenderInElement( /** * For local testing we have two div's that we are rendering into independently. */ -async function setup() { +async function setup(): Promise { + // Since this is a single page Fluid application we are generating a new document id + // if one was not provided + const createNew = window.location.hash.length === 0; + if (createNew) { + window.location.hash = Date.now().toString(); + } + const containerId = window.location.hash.substring(1); + const leftElement = document.getElementById("sbs-left") as HTMLDivElement; if (leftElement === undefined) { throw new Error("sbs-left does not exist"); } - await createContainerAndRenderInElement(leftElement, createNew); + await createContainerAndRenderInElement(containerId, leftElement, createNew); const rightElement = document.getElementById("sbs-right") as HTMLDivElement; if (rightElement === undefined) { throw new Error("sbs-right does not exist"); } // The second time we don't need to createNew because we know a Container exists. - await createContainerAndRenderInElement(rightElement, false); + await createContainerAndRenderInElement(containerId, rightElement, false); // Setting "fluidStarted" is just for our test automation // eslint-disable-next-line @typescript-eslint/dot-notation window["fluidStarted"] = true; } -setup().catch((e) => { - console.error(e); +try { + await setup(); +} catch (error) { + console.error(error); console.log( "%cThere were issues setting up and starting the in memory FLuid Server", "font-size:30px", ); -}); +} diff --git a/examples/service-clients/odsp-client/shared-tree-demo/CHANGELOG.md b/examples/service-clients/odsp-client/shared-tree-demo/CHANGELOG.md index 4844df74a027..74fb70bc10fd 100644 --- a/examples/service-clients/odsp-client/shared-tree-demo/CHANGELOG.md +++ b/examples/service-clients/odsp-client/shared-tree-demo/CHANGELOG.md @@ -1,5 +1,9 @@ # @fluid-example/shared-tree-demo +## 2.2.0 + +Dependency updates only. + ## 2.1.0 Dependency updates only. diff --git a/examples/service-clients/odsp-client/shared-tree-demo/package.json b/examples/service-clients/odsp-client/shared-tree-demo/package.json index fbd3285ae71d..7ba02e9fadef 100644 --- a/examples/service-clients/odsp-client/shared-tree-demo/package.json +++ b/examples/service-clients/odsp-client/shared-tree-demo/package.json @@ -1,6 +1,6 @@ { "name": "@fluid-example/shared-tree-demo", - "version": "2.2.0", + "version": "2.3.0", "private": true, "description": "A shared tree demo using react and odsp client", "homepage": "https://fluidframework.com", @@ -45,9 +45,9 @@ }, "devDependencies": { "@biomejs/biome": "~1.8.3", - "@fluid-tools/build-cli": "0.43.0-285387", + "@fluid-tools/build-cli": "^0.44.0", "@fluidframework/build-common": "^2.0.3", - "@fluidframework/build-tools": "0.43.0-285387", + "@fluidframework/build-tools": "^0.44.0", "@fluidframework/eslint-config-fluid": "^5.3.0", "@types/node": "^18.19.0", "@types/react": "^17.0.44", diff --git a/examples/service-clients/odsp-client/shared-tree-demo/src/fluid.ts b/examples/service-clients/odsp-client/shared-tree-demo/src/fluid.ts index c7be63803416..8ef664c83c45 100644 --- a/examples/service-clients/odsp-client/shared-tree-demo/src/fluid.ts +++ b/examples/service-clients/odsp-client/shared-tree-demo/src/fluid.ts @@ -16,41 +16,33 @@ const client = new OdspClient(clientProps); * * @returns The loaded container and container services. */ -export const loadFluidData = async ( +export async function loadFluidData( itemId: string, - schema: ContainerSchema, + schema: T, ): Promise<{ services: OdspContainerServices; - container: IFluidContainer; -}> => { - const { - container, - services, - }: { container: IFluidContainer; services: OdspContainerServices } = - await client.getContainer(itemId, schema); + container: IFluidContainer; +}> { + const { container, services } = await client.getContainer(itemId, schema); return { services, container }; -}; +} -export const createFluidData = async ( - schema: ContainerSchema, +export async function createFluidData( + schema: T, ): Promise<{ services: OdspContainerServices; - container: IFluidContainer; -}> => { + container: IFluidContainer; +}> { // The client will create a new detached container using the schema // A detached container will enable the app to modify the container before attaching it to the client - const { - container, - services, - }: { container: IFluidContainer; services: OdspContainerServices } = - await client.createContainer(schema); + const { container, services } = await client.createContainer(schema); return { services, container }; -}; +} -export const containerSchema: ContainerSchema = { +export const containerSchema = { initialObjects: { appData: SharedTree, }, -}; +} satisfies ContainerSchema; diff --git a/examples/service-clients/odsp-client/shared-tree-demo/src/index.tsx b/examples/service-clients/odsp-client/shared-tree-demo/src/index.tsx index d28044240a08..d2fd82df45d9 100644 --- a/examples/service-clients/odsp-client/shared-tree-demo/src/index.tsx +++ b/examples/service-clients/odsp-client/shared-tree-demo/src/index.tsx @@ -3,7 +3,7 @@ * Licensed under the MIT License. */ -import { IFluidContainer, ITree } from "fluid-framework"; +import type { IFluidContainer } from "fluid-framework"; import React from "react"; import ReactDOM from "react-dom"; @@ -23,7 +23,7 @@ async function start(): Promise { // a new container. let itemId: string = location.hash.slice(1); const createNew = itemId.length === 0; - let container: IFluidContainer; + let container: IFluidContainer; if (createNew) { ({ container } = await createFluidData(containerSchema)); @@ -31,7 +31,7 @@ async function start(): Promise { ({ container } = await loadFluidData(itemId, containerSchema)); } - const tree = container.initialObjects.appData as ITree; + const tree = container.initialObjects.appData; const appData = tree.viewWith(treeConfiguration); if (createNew) { appData.initialize({ @@ -47,12 +47,7 @@ async function start(): Promise { // the app renders instantly on create new flow. The app will be // interactive immediately. ReactDOM.render( - , + , app, ); @@ -84,12 +79,7 @@ async function start(): Promise { // Update the application state or components without forcing a full page reload ReactDOM.render( - , + , app, ); diff --git a/examples/service-clients/odsp-client/shared-tree-demo/src/reactApp.tsx b/examples/service-clients/odsp-client/shared-tree-demo/src/reactApp.tsx index 4279201403d2..d97c4c63b6e4 100644 --- a/examples/service-clients/odsp-client/shared-tree-demo/src/reactApp.tsx +++ b/examples/service-clients/odsp-client/shared-tree-demo/src/reactApp.tsx @@ -5,7 +5,7 @@ /* eslint-disable prefer-template */ -import { IFluidContainer, Tree, TreeView } from "fluid-framework"; +import { Tree, type TreeView } from "fluid-framework"; import React, { ReactNode, useEffect, useState } from "react"; import { App, Letter } from "./schema.js"; @@ -128,7 +128,6 @@ function TopRow(props: { app: App }): JSX.Element { export function ReactApp(props: { data: TreeView; - container: IFluidContainer; cellSize: { x: number; y: number }; canvasSize: { x: number; y: number }; }): JSX.Element { diff --git a/examples/utils/bundle-size-tests/CHANGELOG.md b/examples/utils/bundle-size-tests/CHANGELOG.md index 616b68400a72..5f72f20398a7 100644 --- a/examples/utils/bundle-size-tests/CHANGELOG.md +++ b/examples/utils/bundle-size-tests/CHANGELOG.md @@ -1,5 +1,9 @@ # @fluid-example/bundle-size-tests +## 2.2.0 + +Dependency updates only. + ## 2.1.0 Dependency updates only. diff --git a/examples/utils/bundle-size-tests/package.json b/examples/utils/bundle-size-tests/package.json index 6722616e719a..3874d4775522 100644 --- a/examples/utils/bundle-size-tests/package.json +++ b/examples/utils/bundle-size-tests/package.json @@ -1,6 +1,6 @@ { "name": "@fluid-example/bundle-size-tests", - "version": "2.2.0", + "version": "2.3.0", "private": true, "description": "A package for understanding the bundle size of Fluid Framework", "homepage": "https://fluidframework.com", @@ -48,10 +48,10 @@ "devDependencies": { "@biomejs/biome": "~1.8.3", "@cerner/duplicate-package-checker-webpack-plugin": "~2.3.0", - "@fluid-tools/version-tools": "0.43.0-285387", + "@fluid-tools/version-tools": "^0.44.0", "@fluidframework/build-common": "^2.0.3", - "@fluidframework/build-tools": "0.43.0-285387", - "@fluidframework/bundle-size-tools": "0.43.0-285387", + "@fluidframework/build-tools": "^0.44.0", + "@fluidframework/bundle-size-tools": "^0.44.0", "@fluidframework/eslint-config-fluid": "^5.3.0", "@mixer/webpack-bundle-compare": "^0.1.0", "@types/node": "^18.19.0", diff --git a/examples/utils/example-utils/CHANGELOG.md b/examples/utils/example-utils/CHANGELOG.md index 6ed5881be3f2..6eb3524055ce 100644 --- a/examples/utils/example-utils/CHANGELOG.md +++ b/examples/utils/example-utils/CHANGELOG.md @@ -1,5 +1,9 @@ # @fluid-example/example-utils +## 2.2.0 + +Dependency updates only. + ## 2.1.0 Dependency updates only. diff --git a/examples/utils/example-utils/package.json b/examples/utils/example-utils/package.json index a15d4a1d46b9..8910234668f8 100644 --- a/examples/utils/example-utils/package.json +++ b/examples/utils/example-utils/package.json @@ -1,6 +1,6 @@ { "name": "@fluid-example/example-utils", - "version": "2.2.0", + "version": "2.3.0", "private": true, "description": "Shared utilities used by examples.", "homepage": "https://fluidframework.com", @@ -80,9 +80,9 @@ "devDependencies": { "@arethetypeswrong/cli": "^0.15.2", "@biomejs/biome": "~1.8.3", - "@fluid-tools/build-cli": "0.43.0-285387", + "@fluid-tools/build-cli": "^0.44.0", "@fluidframework/build-common": "^2.0.3", - "@fluidframework/build-tools": "0.43.0-285387", + "@fluidframework/build-tools": "^0.44.0", "@fluidframework/eslint-config-fluid": "^5.3.0", "@microsoft/api-extractor": "^7.45.1", "@types/react": "^17.0.44", diff --git a/examples/utils/example-utils/src/index.ts b/examples/utils/example-utils/src/index.ts index 09f57ef8f127..39ec3026545a 100644 --- a/examples/utils/example-utils/src/index.ts +++ b/examples/utils/example-utils/src/index.ts @@ -21,13 +21,7 @@ export { } from "./containerViewRuntimeFactory.js"; export type { DataTransformationCallback, - IAcceptedMigrationDetails, IImportExportModel, - IMigratableModel, - IMigrationTool, - IMigrationToolEvents, - IMigrator, - IMigratorEvents, ISameContainerMigratableModel, ISameContainerMigratableModelEvents, ISameContainerMigrationTool, @@ -35,24 +29,22 @@ export type { ISameContainerMigrator, ISameContainerMigratorEvents, IVersionedModel, - MigrationState, SameContainerMigrationState, } from "./migrationInterfaces/index.js"; export { - MigrationToolFactory, SameContainerMigrationTool, SameContainerMigrationToolInstantiationFactory, } from "./migrationTool/index.js"; -export { Migrator, SameContainerMigrator } from "./migrator/index.js"; +export { SameContainerMigrator } from "./migrator/index.js"; export { IDetachedModel, + IModelContainerRuntimeEntryPoint, IModelLoader, ModelContainerRuntimeFactory, ModelLoader, SessionStorageModelLoader, StaticCodeLoader, TinyliciousModelLoader, - IModelContainerRuntimeEntryPoint, } from "./modelLoader/index.js"; export { type IFluidMountableView, diff --git a/examples/utils/example-utils/src/migrationInterfaces/index.ts b/examples/utils/example-utils/src/migrationInterfaces/index.ts index 597024f2a42d..3787ad5f0719 100644 --- a/examples/utils/example-utils/src/migrationInterfaces/index.ts +++ b/examples/utils/example-utils/src/migrationInterfaces/index.ts @@ -5,19 +5,9 @@ export { IImportExportModel, - IMigratableModel, - IVersionedModel, -} from "./migratableModel.js"; -export { - IAcceptedMigrationDetails, - IMigrationTool, - IMigrationToolEvents, - MigrationState, -} from "./migrationTool.js"; -export { DataTransformationCallback, IMigrator, IMigratorEvents } from "./migrator.js"; -export { ISameContainerMigratableModel, ISameContainerMigratableModelEvents, + IVersionedModel, } from "./sameContainerMigratableModel.js"; export { ISameContainerMigrationTool, @@ -25,6 +15,7 @@ export { SameContainerMigrationState, } from "./sameContainerMigrationTool.js"; export { + DataTransformationCallback, ISameContainerMigrator, ISameContainerMigratorEvents, } from "./sameContainerMigrator.js"; diff --git a/examples/utils/example-utils/src/migrationInterfaces/sameContainerMigratableModel.ts b/examples/utils/example-utils/src/migrationInterfaces/sameContainerMigratableModel.ts index d297adcb06c2..d0879b72abba 100644 --- a/examples/utils/example-utils/src/migrationInterfaces/sameContainerMigratableModel.ts +++ b/examples/utils/example-utils/src/migrationInterfaces/sameContainerMigratableModel.ts @@ -6,9 +6,45 @@ import type { IContainer } from "@fluidframework/container-definitions/internal"; import type { IEvent, IEventProvider } from "@fluidframework/core-interfaces"; -import type { IImportExportModel, IVersionedModel } from "./migratableModel.js"; import type { ISameContainerMigrationTool } from "./sameContainerMigrationTool.js"; +/** + * A model with a detectable version. + * + * @remarks + * It's appropriate to use this version to deduce the more specific type of model. + * @internal + */ +export interface IVersionedModel { + /** + * The string version of the model, matching the version of the container code it's paired with. + */ + readonly version: string; +} + +/** + * A model that can import data of ImportType when in detached state, and can also export its data to ExportType. + * @internal + */ +export interface IImportExportModel { + /** + * Permit format checking in a generic manner - without knowing the type of our data or the type of the model, + * we can still check whether the model supports that data. + */ + supportsDataFormat: (initialData: unknown) => initialData is ImportType; + + /** + * importData must be called after initialization but before modifying or attaching the model (i.e. can only + * be called on an unaltered, detached model). + */ + importData: (initialData: ImportType) => Promise; + + /** + * Export the data from the model. Can be passed into importData() for a new container to replicate the data. + */ + exportData: () => Promise; +} + /** * @internal */ diff --git a/examples/utils/example-utils/src/migrationInterfaces/sameContainerMigrator.ts b/examples/utils/example-utils/src/migrationInterfaces/sameContainerMigrator.ts index 9849165bb62f..918668042d68 100644 --- a/examples/utils/example-utils/src/migrationInterfaces/sameContainerMigrator.ts +++ b/examples/utils/example-utils/src/migrationInterfaces/sameContainerMigrator.ts @@ -14,6 +14,7 @@ import type { * The DataTransformationCallback gives an opportunity to modify the exported data before attempting an import * to the new model. The modelVersion is also provided to inform the appropriate transformation to perform. * It is async to permit network calls or lazy-loading the transform logic within the function. + * @internal */ export type DataTransformationCallback = ( exportedData: unknown, diff --git a/examples/utils/example-utils/src/migrationTool/index.ts b/examples/utils/example-utils/src/migrationTool/index.ts index 034930aa6d34..d8964bb60e96 100644 --- a/examples/utils/example-utils/src/migrationTool/index.ts +++ b/examples/utils/example-utils/src/migrationTool/index.ts @@ -3,7 +3,6 @@ * Licensed under the MIT License. */ -export { MigrationToolFactory } from "./migrationTool.js"; export { SameContainerMigrationTool, SameContainerMigrationToolInstantiationFactory, diff --git a/examples/utils/example-utils/src/migrator/index.ts b/examples/utils/example-utils/src/migrator/index.ts index 6577c517ec86..30cf4549ae68 100644 --- a/examples/utils/example-utils/src/migrator/index.ts +++ b/examples/utils/example-utils/src/migrator/index.ts @@ -4,4 +4,3 @@ */ export { SameContainerMigrator } from "./sameContainerMigrator.js"; -export { Migrator } from "./migrator.js"; diff --git a/examples/utils/example-utils/src/modelLoader/index.ts b/examples/utils/example-utils/src/modelLoader/index.ts index 903ef8b46ff3..221acf05e1ea 100644 --- a/examples/utils/example-utils/src/modelLoader/index.ts +++ b/examples/utils/example-utils/src/modelLoader/index.ts @@ -3,7 +3,10 @@ * Licensed under the MIT License. */ -export { IDetachedModel, IModelLoader } from "./interfaces.js"; +export { + IDetachedModel, + IModelLoader, +} from "./interfaces.js"; export { ModelContainerRuntimeFactory, IModelContainerRuntimeEntryPoint, diff --git a/examples/utils/example-utils/src/modelLoader/modelContainerRuntimeFactory.ts b/examples/utils/example-utils/src/modelLoader/modelContainerRuntimeFactory.ts index e382d1ee1f0c..b448bab677b0 100644 --- a/examples/utils/example-utils/src/modelLoader/modelContainerRuntimeFactory.ts +++ b/examples/utils/example-utils/src/modelLoader/modelContainerRuntimeFactory.ts @@ -57,7 +57,6 @@ export abstract class ModelContainerRuntimeFactory implements IRuntim }), runtimeOptions: this.runtimeOptions, existing, - containerScope: context.scope, }); if (!existing) { diff --git a/examples/version-migration/schema-upgrade/.eslintrc.cjs b/examples/utils/migration-tools/.eslintrc.cjs similarity index 59% rename from examples/version-migration/schema-upgrade/.eslintrc.cjs rename to examples/utils/migration-tools/.eslintrc.cjs index 8826fa4dec21..484c63b7e874 100644 --- a/examples/version-migration/schema-upgrade/.eslintrc.cjs +++ b/examples/utils/migration-tools/.eslintrc.cjs @@ -4,9 +4,6 @@ */ module.exports = { - extends: [ - require.resolve("@fluidframework/eslint-config-fluid/minimal-deprecated"), - "prettier", - ], + extends: [require.resolve("@fluidframework/eslint-config-fluid"), "prettier"], rules: {}, }; diff --git a/examples/utils/migration-tools/.gitignore b/examples/utils/migration-tools/.gitignore new file mode 100644 index 000000000000..ee26a5e7bdbf --- /dev/null +++ b/examples/utils/migration-tools/.gitignore @@ -0,0 +1,52 @@ +# Compiled TypeScript and CSS +dist +lib + +# Babel +public/scripts/es5 + +# Logs +logs +*.log + +# Runtime data +pids +*.pid +*.seed + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage + +# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) +.grunt +.cache-loader + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (http://nodejs.org/api/addons.html) +build/Release + +# Dependency directory +# https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git- +node_modules + +# Typings +typings + +# Debug log from npm +npm-debug.log + +# Code coverage +nyc +.nyc_output/ + +# Chart dependencies +**/charts/*.tgz + +# Generated modules +intel_modules/ +temp_modules/ diff --git a/examples/version-migration/schema-upgrade/.npmignore b/examples/utils/migration-tools/.npmignore similarity index 100% rename from examples/version-migration/schema-upgrade/.npmignore rename to examples/utils/migration-tools/.npmignore diff --git a/examples/version-migration/schema-upgrade/LICENSE b/examples/utils/migration-tools/LICENSE similarity index 100% rename from examples/version-migration/schema-upgrade/LICENSE rename to examples/utils/migration-tools/LICENSE diff --git a/examples/utils/migration-tools/README.md b/examples/utils/migration-tools/README.md new file mode 100644 index 000000000000..f2eb4f63cc05 --- /dev/null +++ b/examples/utils/migration-tools/README.md @@ -0,0 +1,48 @@ +# @fluid-example/migration-tools + +This package contains tools for migrating data from one version to another, used by Fluid examples. They are not currently intended for use in production scenarios. + +See [GitHub](https://github.com/microsoft/FluidFramework) for more details on the Fluid Framework and packages within. + + + + + + +## Contribution Guidelines + +There are many ways to [contribute](https://github.com/microsoft/FluidFramework/blob/main/CONTRIBUTING.md) to Fluid. + +- Participate in Q&A in our [GitHub Discussions](https://github.com/microsoft/FluidFramework/discussions). +- [Submit bugs](https://github.com/microsoft/FluidFramework/issues) and help us verify fixes as they are checked in. +- Review the [source code changes](https://github.com/microsoft/FluidFramework/pulls). +- [Contribute bug fixes](https://github.com/microsoft/FluidFramework/blob/main/CONTRIBUTING.md). + +Detailed instructions for working in the repo can be found in the [Wiki](https://github.com/microsoft/FluidFramework/wiki). + +This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). +For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. + +This project may contain Microsoft trademarks or logos for Microsoft projects, products, or services. +Use of these trademarks or logos must follow Microsoft’s [Trademark & Brand Guidelines](https://www.microsoft.com/trademarks). +Use of Microsoft trademarks or logos in modified versions of this project must not cause confusion or imply Microsoft sponsorship. + +## Help + +Not finding what you're looking for in this README? Check out [fluidframework.com](https://fluidframework.com/docs/). + +Still not finding what you're looking for? Please [file an issue](https://github.com/microsoft/FluidFramework/wiki/Submitting-Bugs-and-Feature-Requests). + +Thank you! + +## Trademark + +This project may contain Microsoft trademarks or logos for Microsoft projects, products, or services. + +Use of these trademarks or logos must follow Microsoft's [Trademark & Brand Guidelines](https://www.microsoft.com/en-us/legal/intellectualproperty/trademarks/usage/general). + +Use of Microsoft trademarks or logos in modified versions of this project must not cause confusion or imply Microsoft sponsorship. + + + + diff --git a/examples/utils/migration-tools/api-extractor/api-extractor-lint-alpha.cjs.json b/examples/utils/migration-tools/api-extractor/api-extractor-lint-alpha.cjs.json new file mode 100644 index 000000000000..6e31eb54fe1a --- /dev/null +++ b/examples/utils/migration-tools/api-extractor/api-extractor-lint-alpha.cjs.json @@ -0,0 +1,5 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json", + "extends": "/../../../common/build/build-common/api-extractor-lint.entrypoint.json", + "mainEntryPointFilePath": "/dist/alpha.d.ts" +} diff --git a/examples/utils/migration-tools/api-extractor/api-extractor-lint-alpha.esm.json b/examples/utils/migration-tools/api-extractor/api-extractor-lint-alpha.esm.json new file mode 100644 index 000000000000..985eb4c274d5 --- /dev/null +++ b/examples/utils/migration-tools/api-extractor/api-extractor-lint-alpha.esm.json @@ -0,0 +1,5 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json", + "extends": "/../../../common/build/build-common/api-extractor-lint.entrypoint.json", + "mainEntryPointFilePath": "/lib/alpha.d.ts" +} diff --git a/examples/utils/migration-tools/api-extractor/api-extractor-lint-bundle.json b/examples/utils/migration-tools/api-extractor/api-extractor-lint-bundle.json new file mode 100644 index 000000000000..3ae7ad05c80a --- /dev/null +++ b/examples/utils/migration-tools/api-extractor/api-extractor-lint-bundle.json @@ -0,0 +1,5 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json", + "extends": "/../../../common/build/build-common/api-extractor-lint.json", + "mainEntryPointFilePath": "/lib/index.d.ts" +} diff --git a/examples/version-migration/schema-upgrade/biome.jsonc b/examples/utils/migration-tools/biome.jsonc similarity index 100% rename from examples/version-migration/schema-upgrade/biome.jsonc rename to examples/utils/migration-tools/biome.jsonc diff --git a/examples/utils/migration-tools/package.json b/examples/utils/migration-tools/package.json new file mode 100644 index 000000000000..916671b4e017 --- /dev/null +++ b/examples/utils/migration-tools/package.json @@ -0,0 +1,103 @@ +{ + "name": "@fluid-example/migration-tools", + "version": "2.3.0", + "private": true, + "description": "Tools for migrating data", + "homepage": "https://fluidframework.com", + "repository": { + "type": "git", + "url": "https://github.com/microsoft/FluidFramework.git", + "directory": "examples/utils/migration-tools" + }, + "license": "MIT", + "author": "Microsoft and contributors", + "sideEffects": false, + "type": "module", + "exports": { + "./alpha": { + "import": { + "types": "./lib/alpha.d.ts", + "default": "./lib/index.js" + }, + "require": { + "types": "./dist/alpha.d.ts", + "default": "./dist/index.js" + } + }, + "./internal": { + "import": { + "types": "./lib/index.d.ts", + "default": "./lib/index.js" + }, + "require": { + "types": "./dist/index.d.ts", + "default": "./dist/index.js" + } + } + }, + "scripts": { + "api-extractor:commonjs": "flub generate entrypoints --outDir ./dist", + "api-extractor:esnext": "flub generate entrypoints --outDir ./lib", + "build": "fluid-build . --task build", + "build:compile": "fluid-build . --task compile", + "build:esnext": "tsc --project ./tsconfig.json", + "check:are-the-types-wrong": "echo skip attw --pack .", + "check:biome": "biome check .", + "check:exports": "concurrently \"npm:check:exports:*\"", + "check:exports:bundle-release-tags": "api-extractor run --config api-extractor/api-extractor-lint-bundle.json", + "check:exports:cjs:alpha": "api-extractor run --config api-extractor/api-extractor-lint-alpha.cjs.json", + "check:exports:esm:alpha": "api-extractor run --config api-extractor/api-extractor-lint-alpha.esm.json", + "check:format": "npm run check:biome", + "check:prettier": "prettier --check . --cache --ignore-path ../../../.prettierignore", + "clean": "rimraf --glob dist lib \"**/*.tsbuildinfo\" \"**/*.build.log\" _api-extractor-temp", + "eslint": "eslint --format stylish src", + "eslint:fix": "eslint --format stylish src --fix --fix-type problem,suggestion,layout", + "format": "npm run format:biome", + "format:biome": "biome check . --write", + "format:prettier": "prettier --write . --cache --ignore-path ../../../.prettierignore", + "lint": "fluid-build . --task lint", + "lint:fix": "fluid-build . --task eslint:fix --task format", + "tsc": "fluid-tsc commonjs --project ./tsconfig.cjs.json && copyfiles -f ../../../common/build/build-common/src/cjs/package.json ./dist" + }, + "dependencies": { + "@fluid-experimental/pact-map": "workspace:~", + "@fluid-internal/client-utils": "workspace:~", + "@fluidframework/container-definitions": "workspace:~", + "@fluidframework/container-loader": "workspace:~", + "@fluidframework/container-runtime": "workspace:~", + "@fluidframework/container-runtime-definitions": "workspace:~", + "@fluidframework/core-interfaces": "workspace:~", + "@fluidframework/core-utils": "workspace:~", + "@fluidframework/datastore": "workspace:~", + "@fluidframework/datastore-definitions": "workspace:~", + "@fluidframework/driver-definitions": "workspace:~", + "@fluidframework/local-driver": "workspace:~", + "@fluidframework/register-collection": "workspace:~", + "@fluidframework/routerlicious-driver": "workspace:~", + "@fluidframework/runtime-definitions": "workspace:~", + "@fluidframework/server-local-server": "^5.0.0", + "@fluidframework/task-manager": "workspace:~", + "@fluidframework/tinylicious-driver": "workspace:~", + "uuid": "^9.0.0" + }, + "devDependencies": { + "@arethetypeswrong/cli": "^0.15.2", + "@biomejs/biome": "~1.8.3", + "@fluid-tools/build-cli": "^0.44.0", + "@fluidframework/build-common": "^2.0.3", + "@fluidframework/build-tools": "^0.44.0", + "@fluidframework/eslint-config-fluid": "^5.3.0", + "@microsoft/api-extractor": "^7.45.1", + "@types/uuid": "^9.0.2", + "concurrently": "^8.2.1", + "copyfiles": "^2.4.1", + "eslint": "~8.55.0", + "prettier": "~3.0.3", + "rimraf": "^4.4.0", + "typescript": "~5.4.5" + }, + "typeValidation": { + "disabled": true, + "broken": {} + } +} diff --git a/examples/version-migration/schema-upgrade/prettier.config.cjs b/examples/utils/migration-tools/prettier.config.cjs similarity index 100% rename from examples/version-migration/schema-upgrade/prettier.config.cjs rename to examples/utils/migration-tools/prettier.config.cjs diff --git a/examples/utils/migration-tools/src/index.ts b/examples/utils/migration-tools/src/index.ts new file mode 100644 index 000000000000..2f5c23caa8d3 --- /dev/null +++ b/examples/utils/migration-tools/src/index.ts @@ -0,0 +1,30 @@ +/*! + * Copyright (c) Microsoft Corporation and contributors. All rights reserved. + * Licensed under the MIT License. + */ + +export type { + DataTransformationCallback, + IAcceptedMigrationDetails, + IImportExportModel, + IMigratableModel, + IMigrationTool, + IMigrationToolEvents, + IMigrator, + IMigratorEvents, + IVersionedModel, + MigrationState, +} from "./interfaces/index.js"; +export { MigrationToolFactory } from "./migrationTool.js"; +export { Migrator } from "./migrator.js"; +export { + CreateModelCallback, + IAttachedMigratableModel, + IDetachedMigratableModel, + IMigratableModelContainerRuntimeEntryPoint, + IMigratableModelLoader, + instantiateMigratableRuntime, + MigratableModelLoader, + MigratableSessionStorageModelLoader, + MigratableTinyliciousModelLoader, +} from "./modelLoader/index.js"; diff --git a/examples/utils/migration-tools/src/interfaces/index.ts b/examples/utils/migration-tools/src/interfaces/index.ts new file mode 100644 index 000000000000..fed6d792097a --- /dev/null +++ b/examples/utils/migration-tools/src/interfaces/index.ts @@ -0,0 +1,21 @@ +/*! + * Copyright (c) Microsoft Corporation and contributors. All rights reserved. + * Licensed under the MIT License. + */ + +export { + IImportExportModel, + IMigratableModel, + IVersionedModel, +} from "./migratableModel.js"; +export { + IAcceptedMigrationDetails, + IMigrationTool, + IMigrationToolEvents, + MigrationState, +} from "./migrationTool.js"; +export { + DataTransformationCallback, + IMigrator, + IMigratorEvents, +} from "./migrator.js"; diff --git a/examples/utils/example-utils/src/migrationInterfaces/migratableModel.ts b/examples/utils/migration-tools/src/interfaces/migratableModel.ts similarity index 53% rename from examples/utils/example-utils/src/migrationInterfaces/migratableModel.ts rename to examples/utils/migration-tools/src/interfaces/migratableModel.ts index dd008d393ae0..8d50c344d4ed 100644 --- a/examples/utils/example-utils/src/migrationInterfaces/migratableModel.ts +++ b/examples/utils/migration-tools/src/interfaces/migratableModel.ts @@ -3,10 +3,12 @@ * Licensed under the MIT License. */ -import type { IMigrationTool } from "./migrationTool.js"; - /** - * @internal + * A model with a detectable version. + * + * @remarks + * It's appropriate to use this version to deduce the more specific type of model. + * @alpha */ export interface IVersionedModel { /** @@ -16,7 +18,8 @@ export interface IVersionedModel { } /** - * @internal + * A model that can import data of ImportType when in detached state, and can also export its data to ExportType. + * @alpha */ export interface IImportExportModel { /** @@ -41,19 +44,29 @@ export interface IImportExportModel { // supportsDataFormat() on the callers of importData() (and allow implementers of IMigratableModel to assume // importData() is called with valid data). /** - * @internal + * A model which supports migration via the MigrationTool and Migrator. + * + * @privateRemarks + * A migratable model must have an observable version, which is used to determine if migration is required and to + * identify the source and destination container codes. + * + * It must also support import/export, as this is the mechanism that MigrationTool and Migrator use to perform the + * migration. + * + * Lastly, it should provide dispose capabilities for two purposes: (1) The Migrator will spawn a temporary model + * to export the data, which should be cleaned up after export and (2) After migration is complete, the old model + * is likely no longer needed and should be cleaned up. + * @alpha */ export interface IMigratableModel extends IVersionedModel, IImportExportModel { /** - * The tool that will be used to facilitate the migration. - */ - readonly migrationTool: IMigrationTool; - - /** - * Close the model, rendering it inoperable and closing connections. - * TODO: Decide whether the closing is an integral part of the migration, or if the caller should do the closing. + * Dispose the model, rendering it inoperable and closing connections. + * + * @privateRemarks + * This is required on the interface because the Migrator will make its own instance of the model for export, + * and needs to clean that model up after the export is done. */ - close(): void; + dispose(): void; } diff --git a/examples/utils/example-utils/src/migrationInterfaces/migrationTool.ts b/examples/utils/migration-tools/src/interfaces/migrationTool.ts similarity index 98% rename from examples/utils/example-utils/src/migrationInterfaces/migrationTool.ts rename to examples/utils/migration-tools/src/interfaces/migrationTool.ts index e8c327e64fc7..04240b788734 100644 --- a/examples/utils/example-utils/src/migrationInterfaces/migrationTool.ts +++ b/examples/utils/migration-tools/src/interfaces/migrationTool.ts @@ -11,14 +11,14 @@ import type { IEvent, IEventProvider } from "@fluidframework/core-interfaces"; * * stopping - a proposal to migrate has been made, but not accepted yet. The client must stop sending data. * * migrating - a proposal to migrate has been accepted. The data is currently being migrated. * * migrated - migration has completed and the new container is available. - * @internal + * @alpha */ export type MigrationState = "collaborating" | "stopping" | "migrating" | "migrated"; /** * The details of the accepted migration. Signifies that the collaboration has agreed to migrate whatever * data was present at sequence number migrationSequenceNumber to use version newVersion. - * @internal + * @alpha */ export interface IAcceptedMigrationDetails { /** @@ -32,7 +32,7 @@ export interface IAcceptedMigrationDetails { } /** - * @internal + * @alpha */ export interface IMigrationToolEvents extends IEvent { (event: "stopping" | "migrating" | "migrated", listener: () => void); @@ -41,7 +41,7 @@ export interface IMigrationToolEvents extends IEvent { } /** - * @internal + * @alpha */ export interface IMigrationTool { readonly events: IEventProvider; diff --git a/examples/utils/example-utils/src/migrationInterfaces/migrator.ts b/examples/utils/migration-tools/src/interfaces/migrator.ts similarity index 90% rename from examples/utils/example-utils/src/migrationInterfaces/migrator.ts rename to examples/utils/migration-tools/src/interfaces/migrator.ts index 3546fa40a621..b4a898211a70 100644 --- a/examples/utils/example-utils/src/migrationInterfaces/migrator.ts +++ b/examples/utils/migration-tools/src/interfaces/migrator.ts @@ -5,13 +5,14 @@ import type { IEvent, IEventProvider } from "@fluidframework/core-interfaces"; -import type { IMigratableModel, MigrationState } from "../migrationInterfaces/index.js"; +import type { IMigratableModel } from "./migratableModel.js"; +import type { MigrationState } from "./migrationTool.js"; /** * The DataTransformationCallback gives an opportunity to modify the exported data before attempting an import * to the new model. The modelVersion is also provided to inform the appropriate transformation to perform. * It is async to permit network calls or lazy-loading the transform logic within the function. - * @internal + * @alpha */ export type DataTransformationCallback = ( exportedData: unknown, @@ -19,7 +20,7 @@ export type DataTransformationCallback = ( ) => Promise; /** - * @internal + * @alpha */ export interface IMigratorEvents extends IEvent { (event: "migrated" | "migrating", listener: () => void); @@ -27,7 +28,7 @@ export interface IMigratorEvents extends IEvent { } /** - * @internal + * @alpha */ export interface IMigrator { readonly events: IEventProvider; diff --git a/examples/utils/example-utils/src/migrationTool/migrationTool.ts b/examples/utils/migration-tools/src/migrationTool.ts similarity index 92% rename from examples/utils/example-utils/src/migrationTool/migrationTool.ts rename to examples/utils/migration-tools/src/migrationTool.ts index a6f20f65c3ef..6c8bb520df04 100644 --- a/examples/utils/example-utils/src/migrationTool/migrationTool.ts +++ b/examples/utils/migration-tools/src/migrationTool.ts @@ -5,7 +5,11 @@ import { IPactMap, PactMap } from "@fluid-experimental/pact-map"; import { TypedEventEmitter } from "@fluid-internal/client-utils"; -import type { IEventProvider } from "@fluidframework/core-interfaces"; +import type { + FluidObject, + IEventProvider, + IFluidHandle, +} from "@fluidframework/core-interfaces"; import { assert } from "@fluidframework/core-utils/internal"; import { FluidDataStoreRuntime } from "@fluidframework/datastore/internal"; import type { @@ -24,10 +28,11 @@ import type { import { ITaskManager, TaskManager } from "@fluidframework/task-manager/internal"; import type { + IAcceptedMigrationDetails, IMigrationTool, IMigrationToolEvents, MigrationState, -} from "../migrationInterfaces/index.js"; +} from "./interfaces/index.js"; const consensusRegisterCollectionId = "consensus-register-collection"; const pactMapId = "pact-map"; @@ -40,7 +45,7 @@ const newContainerIdKey = "newContainerId"; class MigrationTool implements IMigrationTool { private _disposed = false; - public get disposed() { + public get disposed(): boolean { return this._disposed; } @@ -49,11 +54,11 @@ class MigrationTool implements IMigrationTool { return this._events; } - public get connected() { + public get connected(): boolean { return this.runtime.connected; } - public get handle() { + public get handle(): IFluidHandle { // MigrationToolFactory already provides an entryPoint initialization function to the data store runtime, // so this object should always have access to a non-null entryPoint. assert(this.runtime.entryPoint !== undefined, "EntryPoint was undefined"); @@ -65,6 +70,7 @@ class MigrationTool implements IMigrationTool { return "migrated"; } else if (this.acceptedMigration !== undefined) { return "migrating"; + // eslint-disable-next-line unicorn/no-negated-condition } else if (this.proposedVersion !== undefined) { return "stopping"; } else { @@ -72,7 +78,7 @@ class MigrationTool implements IMigrationTool { } } - public get newContainerId() { + public get newContainerId(): string | undefined { return this.consensusRegisterCollection.read(newContainerIdKey); } @@ -118,7 +124,7 @@ class MigrationTool implements IMigrationTool { } } - public async finalizeMigration(id: string) { + public async finalizeMigration(id: string): Promise { // Only permit a single container to be set as a migration destination. if (this.consensusRegisterCollection.read(newContainerIdKey) !== undefined) { throw new Error("New container was already established"); @@ -130,11 +136,11 @@ class MigrationTool implements IMigrationTool { await this.consensusRegisterCollection.write(newContainerIdKey, id); } - public get proposedVersion() { + public get proposedVersion(): string | undefined { return this.pactMap.getPending(newVersionKey) ?? this.pactMap.get(newVersionKey); } - public get acceptedMigration() { + public get acceptedMigration(): IAcceptedMigrationDetails | undefined { const migrationDetails = this.pactMap.getWithDetails(newVersionKey); if (migrationDetails === undefined) { return undefined; @@ -150,7 +156,7 @@ class MigrationTool implements IMigrationTool { }; } - public readonly proposeVersion = (newVersion: string) => { + public readonly proposeVersion = (newVersion: string): void => { // Don't permit changes to the version after a new one has already been accepted. // TODO: Consider whether we should throw on trying to set when a pending proposal exists -- currently // the PactMap will silently drop these on the floor. @@ -196,14 +202,14 @@ const migrationToolSharedObjectRegistry = new Map([ ]); /** - * @internal + * @alpha */ export class MigrationToolFactory implements IFluidDataStoreFactory { public get type(): string { throw new Error("Do not use the type on the data store factory"); } - public get IFluidDataStoreFactory() { + public get IFluidDataStoreFactory(): IFluidDataStoreFactory { return this; } diff --git a/examples/utils/example-utils/src/migrator/migrator.ts b/examples/utils/migration-tools/src/migrator.ts similarity index 75% rename from examples/utils/example-utils/src/migrator/migrator.ts rename to examples/utils/migration-tools/src/migrator.ts index 6e47bcfae51b..98f4654717eb 100644 --- a/examples/utils/example-utils/src/migrator/migrator.ts +++ b/examples/utils/migration-tools/src/migrator.ts @@ -10,32 +10,49 @@ import { assert } from "@fluidframework/core-utils/internal"; import type { DataTransformationCallback, IMigratableModel, + IMigrationTool, IMigrator, IMigratorEvents, MigrationState, -} from "../migrationInterfaces/index.js"; -import type { IDetachedModel, IModelLoader } from "../modelLoader/index.js"; +} from "./interfaces/index.js"; +import type { IDetachedMigratableModel, IMigratableModelLoader } from "./modelLoader/index.js"; /** - * @internal + * As the Migrator migrates, it updates its reference to the current version of the model. + * This interface describes the characteristics of the model that it's tracking in a single object, + * which will be swapped out atomically as the migration happens. + */ +interface MigratableParts { + model: IMigratableModel; + migrationTool: IMigrationTool; + id: string; +} + +/** + * The Migrator maintains a reference to the current model, and interacts with it (and its MigrationTool) + * to detect, observe, trigger, and execute migration as appropriate. + * @alpha */ export class Migrator implements IMigrator { - private _currentModel: IMigratableModel; + private _currentMigratable: MigratableParts; public get currentModel(): IMigratableModel { - return this._currentModel; + return this._currentMigratable.model; + } + + public get currentMigrationTool(): IMigrationTool { + return this._currentMigratable.migrationTool; } - private _currentModelId: string; public get currentModelId(): string { - return this._currentModelId; + return this._currentMigratable.id; } public get migrationState(): MigrationState { - return this._currentModel.migrationTool.migrationState; + return this.currentMigrationTool.migrationState; } public get connected(): boolean { - return this._currentModel.migrationTool.connected; + return this.currentMigrationTool.connected; } private readonly _events = new TypedEventEmitter(); @@ -58,7 +75,7 @@ export class Migrator implements IMigrator { /** * Detached model that is ready to attach. This is stored for retry scenarios. */ - private _preparedDetachedModel: IDetachedModel | undefined; + private _preparedDetachedModel: IDetachedMigratableModel | undefined; /** * After attaching the prepared model, but before we have written its ID into the current model, we'll store the ID @@ -67,13 +84,17 @@ export class Migrator implements IMigrator { private _preparedModelId: string | undefined; public constructor( - private readonly modelLoader: IModelLoader, + private readonly modelLoader: IMigratableModelLoader, initialMigratable: IMigratableModel, + initialMigrationTool: IMigrationTool, initialId: string, private readonly dataTransformationCallback?: DataTransformationCallback, ) { - this._currentModel = initialMigratable; - this._currentModelId = initialId; + this._currentMigratable = { + model: initialMigratable, + migrationTool: initialMigrationTool, + id: initialId, + }; this.takeAppropriateActionForCurrentMigratable(); } @@ -83,28 +104,28 @@ export class Migrator implements IMigrator { * in the process of migrating or already migrated (and thus we need to load again). It is not safe to assume * that a freshly-loaded migrated container is in collaborating state. */ - private readonly takeAppropriateActionForCurrentMigratable = () => { - const migrationState = this._currentModel.migrationTool.migrationState; + private readonly takeAppropriateActionForCurrentMigratable = (): void => { + const migrationState = this.currentMigrationTool.migrationState; if (migrationState === "migrating") { this.ensureMigrating(); } else if (migrationState === "migrated") { this.ensureLoading(); } else { - this._currentModel.migrationTool.events.once( + this.currentMigrationTool.events.once( "migrating", this.takeAppropriateActionForCurrentMigratable, ); } }; - private readonly ensureMigrating = () => { + private readonly ensureMigrating = (): void => { // ensureMigrating() is called when we reach the "migrating" state. This should likely only happen once, but // can happen multiple times if we disconnect during the migration process. if (!this.connected) { // If we are not connected we should wait until we reconnect and try again. Note: we re-enter the state // machine, since it's possible another client has already completed the migration by the time we reconnect. - this.currentModel.migrationTool.events.once( + this.currentMigrationTool.events.once( "connected", this.takeAppropriateActionForCurrentMigratable, ); @@ -119,25 +140,25 @@ export class Migrator implements IMigrator { throw new Error("Cannot perform migration, we are currently trying to load"); } - const migratable = this._currentModel; - const acceptedMigration = migratable.migrationTool.acceptedMigration; + const migrationTool = this.currentMigrationTool; + const acceptedMigration = migrationTool.acceptedMigration; if (acceptedMigration === undefined) { throw new Error("Expect an accepted migration before migration starts"); } - const doTheMigration = async () => { + const doTheMigration = async (): Promise => { // doTheMigration() is called at the start of migration and should only resolve in two cases. First, is if // either the local or another client successfully completes the migration. Second, is if we disconnect // during the migration process. In both cases we should re-enter the state machine and take the // appropriate action (see then() block below). - const prepareTheMigration = async () => { + const prepareTheMigration = async (): Promise => { // It's possible that our modelLoader is older and doesn't understand the new acceptedMigration. // Currently this fails the migration gracefully and emits an event so the app developer can know // they're stuck. Ideally the app developer would find a way to acquire a new ModelLoader and move // forward, or at least advise the end user to refresh the page or something. // TODO: Does the app developer have everything they need to dispose gracefully when recovering with - // a new ModelLoader? + // a new MigratableModelLoader? const migrationSupported = await this.modelLoader.supportsVersion( acceptedMigration.newVersion, ); @@ -160,21 +181,22 @@ export class Migrator implements IMigrator { // 2. Have the paused loading logic know how to load a different older snapshot version (though old versions may get deleted). // 3. Have a acceptance rollback or acceptance update path, to either retry or update the acceptance sequence number to be reachable // 4. Use a non-paused load, and accept that some late-arriving data might get included. - const exportModel = await this.modelLoader.loadExistingPaused( - this._currentModelId, + const { model: exportModel } = await this.modelLoader.loadExistingPaused( + this.currentModelId, acceptedMigration.migrationSequenceNumber, ); const exportedData = await exportModel.exportData(); - exportModel.close(); + exportModel.dispose(); // TODO: Is there a reasonable way to validate at proposal time whether we'll be able to get the // exported data into a format that the new model can import? If we can determine it early, then - // clients with old ModelLoaders can use that opportunity to dispose early and try to get new - // ModelLoaders. + // clients with old MigratableModelLoaders can use that opportunity to dispose early and try to get new + // MigratableModelLoaders. let transformedData: unknown; if (migratedModel.supportsDataFormat(exportedData)) { // If the migrated model already supports the data format, go ahead with the migration. transformedData = exportedData; + // eslint-disable-next-line unicorn/no-negated-condition } else if (this.dataTransformationCallback !== undefined) { // Otherwise, try using the dataTransformationCallback if provided to get the exported data into // a format that we can import. @@ -202,7 +224,7 @@ export class Migrator implements IMigrator { this._preparedDetachedModel = detachedModel; }; - const completeTheMigration = async () => { + const completeTheMigration = async (): Promise => { assert( this._preparedDetachedModel !== undefined, "this._preparedDetachedModel should be defined", @@ -211,8 +233,8 @@ export class Migrator implements IMigrator { // Volunteer to complete the migration. let isAssigned: boolean; try { - isAssigned = await this.currentModel.migrationTool.volunteerForMigration(); - } catch (error) { + isAssigned = await this.currentMigrationTool.volunteerForMigration(); + } catch { // volunteerForMigration() will throw an error on disconnection. In this case, we should exit and // re-enter the state machine which will wait until we reconnect. // Note: while we wait to reconnect it is possible that another client will have already completed @@ -221,7 +243,7 @@ export class Migrator implements IMigrator { return; } - if (this.currentModel.migrationTool.newContainerId !== undefined) { + if (this.currentMigrationTool.newContainerId !== undefined) { // If newContainerId is already set, then another client already completed the migration. return; } @@ -233,14 +255,14 @@ export class Migrator implements IMigrator { } // Check to make sure we still have the task assignment. - if (!this.currentModel.migrationTool.haveMigrationTask()) { + if (!this.currentMigrationTool.haveMigrationTask()) { // Exit early if we lost the task assignment, we are most likely disconnected. return; } - await migratable.migrationTool.finalizeMigration(this._preparedModelId); + await migrationTool.finalizeMigration(this._preparedModelId); - this.currentModel.migrationTool.completeMigrationTask(); + this.currentMigrationTool.completeMigrationTask(); }; // Prepare the detached model if we haven't already. @@ -266,7 +288,7 @@ export class Migrator implements IMigrator { // We assume if we are still connected after exiting the loop, then we should be in the "migrated" // state. The following assert validates this assumption. assert( - this.currentModel.migrationTool.newContainerId !== undefined, + this.currentMigrationTool.newContainerId !== undefined, "newContainerId should be defined", ); } @@ -276,7 +298,7 @@ export class Migrator implements IMigrator { .catch(console.error); }; - private readonly ensureLoading = () => { + private readonly ensureLoading = (): void => { // We assume ensureLoading() is called a single time after we reach the "migrated" state. if (this._migratedLoadP !== undefined) { @@ -287,18 +309,18 @@ export class Migrator implements IMigrator { throw new Error("Cannot start loading the migrated before migration is complete"); } - const migratable = this._currentModel; - const acceptedMigration = migratable.migrationTool.acceptedMigration; + const migrationTool = this.currentMigrationTool; + const acceptedMigration = migrationTool.acceptedMigration; if (acceptedMigration === undefined) { throw new Error("Expect an accepted version before migration starts"); } - const migratedId = migratable.migrationTool.newContainerId; + const migratedId = migrationTool.newContainerId; if (migratedId === undefined) { throw new Error("Migration ended without a new container being created"); } - const doTheLoad = async () => { + const doTheLoad = async (): Promise => { // doTheLoad() should only be called once. It will resolve once we complete loading. const migrationSupported = await this.modelLoader.supportsVersion( @@ -309,14 +331,18 @@ export class Migrator implements IMigrator { this._migratedLoadP = undefined; return; } - const migrated = await this.modelLoader.loadExisting(migratedId); - // Note: I'm choosing not to close the old migratable here, and instead allow the lifecycle management + const { model: migratedModel, migrationTool: migratedMigrationTool } = + await this.modelLoader.loadExisting(migratedId); + // Note: I'm choosing not to dispose the old migratable here, and instead allow the lifecycle management // of the migratable to be the responsibility of whoever created the Migrator (and handed it its first - // migratable). It could also be fine to close here, just need to have an explicit contract to clarify + // migratable). It could also be fine to dispose here, just need to have an explicit contract to clarify // who is responsible for managing that. - this._currentModel = migrated; - this._currentModelId = migratedId; - this._events.emit("migrated", migrated, migratedId); + this._currentMigratable = { + model: migratedModel, + migrationTool: migratedMigrationTool, + id: migratedId, + }; + this._events.emit("migrated", migratedModel, migratedId); this._migratedLoadP = undefined; // Reset retry values diff --git a/examples/utils/migration-tools/src/modelLoader/index.ts b/examples/utils/migration-tools/src/modelLoader/index.ts new file mode 100644 index 000000000000..0c979fcba582 --- /dev/null +++ b/examples/utils/migration-tools/src/modelLoader/index.ts @@ -0,0 +1,18 @@ +/*! + * Copyright (c) Microsoft Corporation and contributors. All rights reserved. + * Licensed under the MIT License. + */ + +export { + CreateModelCallback, + IMigratableModelContainerRuntimeEntryPoint, + instantiateMigratableRuntime, +} from "./instantiateMigratableRuntime.js"; +export { + IAttachedMigratableModel, + IDetachedMigratableModel, + IMigratableModelLoader, +} from "./interfaces.js"; +export { MigratableModelLoader } from "./migratableModelLoader.js"; +export { MigratableSessionStorageModelLoader } from "./migratableSessionStorageModelLoader.js"; +export { MigratableTinyliciousModelLoader } from "./migratableTinyliciousModelLoader.js"; diff --git a/examples/utils/migration-tools/src/modelLoader/instantiateMigratableRuntime.ts b/examples/utils/migration-tools/src/modelLoader/instantiateMigratableRuntime.ts new file mode 100644 index 000000000000..7ce8d0ebb4b4 --- /dev/null +++ b/examples/utils/migration-tools/src/modelLoader/instantiateMigratableRuntime.ts @@ -0,0 +1,110 @@ +/*! + * Copyright (c) Microsoft Corporation and contributors. All rights reserved. + * Licensed under the MIT License. + */ + +import { + IContainer, + IContainerContext, + IRuntime, +} from "@fluidframework/container-definitions/internal"; +import { + ContainerRuntime, + IContainerRuntimeOptions, +} from "@fluidframework/container-runtime/internal"; +import { IContainerRuntime } from "@fluidframework/container-runtime-definitions/internal"; +import type { IFluidHandle } from "@fluidframework/core-interfaces"; +import { NamedFluidDataStoreRegistryEntries } from "@fluidframework/runtime-definitions/internal"; + +import type { IMigrationTool } from "../interfaces/index.js"; +import { MigrationToolFactory } from "../migrationTool.js"; + +async function getDataStoreEntryPoint( + containerRuntime: IContainerRuntime, + alias: string, +): Promise { + const entryPointHandle = (await containerRuntime.getAliasedDataStoreEntryPoint(alias)) as + | IFluidHandle + | undefined; + + if (entryPointHandle === undefined) { + throw new Error(`Default dataStore [${alias}] must exist`); + } + + return entryPointHandle.get(); +} + +/** + * The CreateModelCallback should use the passed runtime and container to construct the model that the + * host app will interact with. + * @alpha + */ +export type CreateModelCallback = ( + runtime: IContainerRuntime, + container: IContainer, +) => Promise; + +/** + * @privateRemarks + * The MigratableModelLoader expects to work with container runtimes whose entry point conforms to + * this interface. + * @alpha + */ +export interface IMigratableModelContainerRuntimeEntryPoint { + getModelAndMigrationTool( + container: IContainer, + ): Promise<{ model: T; migrationTool: IMigrationTool }>; +} + +const migrationToolId = "migration-tool"; + +const migrationToolRegistryKey = "migration-tool"; +const migrationToolFactory = new MigrationToolFactory(); + +/** + * This helper should be used as a stand-in for ContainerRuntime.loadRuntime when using Migrator and MigratableModelLoader. + * + * @privateRemarks + * In addition to what ContainerRuntime.loadRuntime does, this adds in and correctly initializes the migration tools that + * Migrator expects to interact with, and exposes an entrypoint that MigratableModelLoader expects to find. + * TODO: Consider switching to a property bag for parameters. + * @alpha + */ +export const instantiateMigratableRuntime = async ( + context: IContainerContext, + existing: boolean, + registryEntries: NamedFluidDataStoreRegistryEntries, + createModel: CreateModelCallback, + runtimeOptions?: IContainerRuntimeOptions, +): Promise => { + const combinedRegistryEntries: NamedFluidDataStoreRegistryEntries = [ + ...registryEntries, + [migrationToolRegistryKey, Promise.resolve(migrationToolFactory)], + ]; + const runtime = await ContainerRuntime.loadRuntime({ + context, + registryEntries: combinedRegistryEntries, + provideEntryPoint: async ( + containerRuntime: IContainerRuntime, + ): Promise> => ({ + getModelAndMigrationTool: async (container: IContainer) => ({ + // TODO: Think about the timing and order of the awaits + model: await createModel(containerRuntime, container), + migrationTool: await getDataStoreEntryPoint(containerRuntime, migrationToolId), + }), + }), + runtimeOptions, + existing, + }); + + if (!existing) { + const migrationTool = await runtime.createDataStore(migrationToolRegistryKey); + await migrationTool.trySetAlias(migrationToolId); + } + // Force the MigrationTool to instantiate in all cases. The PactMap it uses must be loaded and running in + // order to respond with accept ops, and without this call the MigrationTool won't be instantiated on the + // summarizer client. + await getDataStoreEntryPoint(runtime, migrationToolId); + + return runtime; +}; diff --git a/examples/utils/migration-tools/src/modelLoader/interfaces.ts b/examples/utils/migration-tools/src/modelLoader/interfaces.ts new file mode 100644 index 000000000000..60123bbe6cd0 --- /dev/null +++ b/examples/utils/migration-tools/src/modelLoader/interfaces.ts @@ -0,0 +1,80 @@ +/*! + * Copyright (c) Microsoft Corporation and contributors. All rights reserved. + * Licensed under the MIT License. + */ + +import type { IMigrationTool } from "../interfaces/index.js"; + +// TODO: Consider just extending IAttachedMigratableModel +/** + * Object returned from calling IModelLoader.createDetached(). + * @alpha + */ +export interface IDetachedMigratableModel { + /** + * The newly created, detached model object. + */ + model: ModelType; + /** + * The migration tool that will be used to migrate away from this model. + */ + migrationTool: IMigrationTool; + /** + * A function that will attach the model object to the service when called. + * @returns a Promise that will resolve after attach completes with the container ID of the newly attached + * container. + */ + attach: () => Promise; +} + +/** + * Object returned from calling IModelLoader.createDetached(). + * @alpha + */ +export interface IAttachedMigratableModel { + /** + * The newly created, detached model object. + */ + model: ModelType; + /** + * The migration tool that will be used to migrate away from this model. + */ + migrationTool: IMigrationTool; +} + +/** + * @alpha + */ +export interface IMigratableModelLoader { + /** + * Check if the IMigratableModelLoader knows how to instantiate an appropriate model for the provided container code version. + * It is async to permit dynamic model loading - e.g. referring to a remote service to determine if the requested + * model is available. + * @param version - the container code version to check + */ + supportsVersion(version: string): Promise; + + /** + * Create a detached model using the specified version of container code. + * Returns an object containing the detached model plus an attach callback. When invoked, the attach callback + * returns a promise that will resolve after attach has completed with the id of the container. + * @param version - the container code version to create a model for + */ + createDetached(version: string): Promise>; + + /** + * Load a model for the container with the given id. + * @param id - the id of the container to load + */ + loadExisting(id: string): Promise>; + + /** + * Load a model for the container with the given id. + * @param id - the id of the container to load + * @param sequenceNumber - the sequence number we want to load to and pause at + */ + loadExistingPaused( + id: string, + sequenceNumber: number, + ): Promise>; +} diff --git a/examples/utils/migration-tools/src/modelLoader/migratableModelLoader.ts b/examples/utils/migration-tools/src/modelLoader/migratableModelLoader.ts new file mode 100644 index 000000000000..f5fd60f3b26f --- /dev/null +++ b/examples/utils/migration-tools/src/modelLoader/migratableModelLoader.ts @@ -0,0 +1,144 @@ +/*! + * Copyright (c) Microsoft Corporation and contributors. All rights reserved. + * Licensed under the MIT License. + */ + +import { + type IContainer, + type IHostLoader, + LoaderHeader, +} from "@fluidframework/container-definitions/internal"; +import { + ILoaderProps, + Loader, + loadContainerPaused, +} from "@fluidframework/container-loader/internal"; +import type { IRequest } from "@fluidframework/core-interfaces"; + +import { type IMigratableModelContainerRuntimeEntryPoint } from "./instantiateMigratableRuntime.js"; +import type { + IAttachedMigratableModel, + IDetachedMigratableModel, + IMigratableModelLoader, +} from "./interfaces.js"; + +/** + * @alpha + */ +export class MigratableModelLoader implements IMigratableModelLoader { + private readonly loader: IHostLoader; + private readonly generateCreateNewRequest: () => IRequest; + + // TODO: See if there's a nicer way to parameterize the createNew request. + // Here we specifically pick just the loader props we know we need to keep API exposure low. Fine to add more + // here if we determine they're needed, but they should be picked explicitly (e.g. avoid "scope"). + public constructor( + props: Pick< + ILoaderProps, + "urlResolver" | "documentServiceFactory" | "codeLoader" | "logger" + > & { + generateCreateNewRequest: () => IRequest; + }, + ) { + this.loader = new Loader({ + urlResolver: props.urlResolver, + documentServiceFactory: props.documentServiceFactory, + codeLoader: props.codeLoader, + logger: props.logger, + }); + this.generateCreateNewRequest = props.generateCreateNewRequest; + } + + public async supportsVersion(version: string): Promise { + // To answer the question of whether we support a given version, we would need to query the codeLoader + // to see if it thinks it can load the requested version. But for now, ICodeDetailsLoader doesn't have + // a supports() method. We could attempt a load and catch the error, but it might not be desirable to + // load code just to check. It might be desirable to add a supports() method to ICodeDetailsLoader. + return true; + } + + /** + * The purpose of the model pattern and the model loader is to wrap the IContainer in a more useful object and + * interface. This demo uses a convention of the entrypoint providing a getModelAndMigrationTool method to do so. + * It does this with the expectation that the model has been bundled with the container code. + * + * Other strategies to obtain the wrapping model could also work fine here - for example a standalone model code + * loader that separately fetches model code and wraps the container from the outside. + */ + private async getModelAndMigrationToolFromContainer( + container: IContainer, + ): Promise> { + const entryPoint = + (await container.getEntryPoint()) as IMigratableModelContainerRuntimeEntryPoint; + // If the user tries to use this model loader with an incompatible container runtime, we want to give them + // a comprehensible error message. So distrust the type by default and do some basic type checking. + if (typeof entryPoint.getModelAndMigrationTool !== "function") { + throw new TypeError( + "Incompatible container runtime: doesn't provide getModelAndMigrationTool", + ); + } + const modelAndMigrationTool = await entryPoint.getModelAndMigrationTool(container); + if (typeof modelAndMigrationTool.model !== "object") { + throw new TypeError("Incompatible container runtime: doesn't provide model"); + } + if (typeof modelAndMigrationTool.migrationTool !== "object") { + throw new TypeError("Incompatible container runtime: doesn't provide migrationTool"); + } + return modelAndMigrationTool; + } + + // It would be preferable for attaching to look more like service.attach(model) rather than returning an attach + // callback here, but this callback at least allows us to keep the method off the model interface. + // TODO: Consider making the version param optional, and in that case having a mechanism to query the codeLoader + // for the latest/default version to use? + public async createDetached(version: string): Promise> { + const container = await this.loader.createDetachedContainer({ package: version }); + const { model, migrationTool } = + await this.getModelAndMigrationToolFromContainer(container); + // The attach callback lets us defer the attach so the caller can do whatever initialization pre-attach, + // without leaking out the loader, service, etc. We also return the container ID here so we don't have + // to stamp it on something that would rather not know it (e.g. the model). + const attach = async (): Promise => { + await container.attach(this.generateCreateNewRequest()); + if (container.resolvedUrl === undefined) { + throw new Error("Resolved Url not available on attached container"); + } + return container.resolvedUrl.id; + }; + return { model, migrationTool, attach }; + } + + public async loadExisting(id: string): Promise> { + const container = await this.loader.resolve({ + url: id, + headers: { + [LoaderHeader.loadMode]: { + // Here we use "all" to ensure we are caught up before returning. This is particularly important + // for direct-link scenarios, where the user might have a direct link to a data object that was + // just attached (i.e. the "attach" op and the "set" of the handle into some map is in the + // trailing ops). If we don't fully process those ops, the expected object won't be found. + opsBeforeReturn: "all", + }, + }, + }); + const { model, migrationTool } = + await this.getModelAndMigrationToolFromContainer(container); + return { model, migrationTool }; + } + + public async loadExistingPaused( + id: string, + sequenceNumber: number, + ): Promise> { + const container = await loadContainerPaused( + this.loader, + { + url: id, + }, + sequenceNumber, + ); + const { model, migrationTool } = + await this.getModelAndMigrationToolFromContainer(container); + return { model, migrationTool }; + } +} diff --git a/examples/utils/migration-tools/src/modelLoader/migratableSessionStorageModelLoader.ts b/examples/utils/migration-tools/src/modelLoader/migratableSessionStorageModelLoader.ts new file mode 100644 index 000000000000..58e899cab9f2 --- /dev/null +++ b/examples/utils/migration-tools/src/modelLoader/migratableSessionStorageModelLoader.ts @@ -0,0 +1,90 @@ +/*! + * Copyright (c) Microsoft Corporation and contributors. All rights reserved. + * Licensed under the MIT License. + */ + +import { ICodeDetailsLoader } from "@fluidframework/container-definitions/internal"; +import { ITelemetryBaseLogger } from "@fluidframework/core-interfaces"; +import { IDocumentServiceFactory } from "@fluidframework/driver-definitions/internal"; +import { + LocalDocumentServiceFactory, + LocalResolver, + LocalSessionStorageDbFactory, + createLocalResolverCreateNewRequest, +} from "@fluidframework/local-driver/internal"; +import { + ILocalDeltaConnectionServer, + LocalDeltaConnectionServer, +} from "@fluidframework/server-local-server"; +import { v4 as uuid } from "uuid"; + +import type { + IAttachedMigratableModel, + IDetachedMigratableModel, + IMigratableModelLoader, +} from "./interfaces.js"; +import { MigratableModelLoader } from "./migratableModelLoader.js"; + +const urlResolver = new LocalResolver(); + +const deltaConnectionServerMap = new Map(); +const getDocumentServiceFactory = (documentId: string): IDocumentServiceFactory => { + let deltaConnection = deltaConnectionServerMap.get(documentId); + if (deltaConnection === undefined) { + deltaConnection = LocalDeltaConnectionServer.create(new LocalSessionStorageDbFactory()); + deltaConnectionServerMap.set(documentId, deltaConnection); + } + + return new LocalDocumentServiceFactory(deltaConnection); +}; + +/** + * @alpha + */ +export class MigratableSessionStorageModelLoader + implements IMigratableModelLoader +{ + public constructor( + private readonly codeLoader: ICodeDetailsLoader, + private readonly logger?: ITelemetryBaseLogger, + ) {} + + public async supportsVersion(version: string): Promise { + return true; + } + + public async createDetached(version: string): Promise> { + const documentId = uuid(); + const modelLoader = new MigratableModelLoader({ + urlResolver, + documentServiceFactory: getDocumentServiceFactory(documentId), + codeLoader: this.codeLoader, + logger: this.logger, + generateCreateNewRequest: () => createLocalResolverCreateNewRequest(documentId), + }); + return modelLoader.createDetached(version); + } + public async loadExisting(id: string): Promise> { + const documentId = id; + const modelLoader = new MigratableModelLoader({ + urlResolver, + documentServiceFactory: getDocumentServiceFactory(documentId), + codeLoader: this.codeLoader, + logger: this.logger, + generateCreateNewRequest: () => createLocalResolverCreateNewRequest(documentId), + }); + return modelLoader.loadExisting(`${window.location.origin}/${id}`); + } + public async loadExistingPaused( + id: string, + sequenceNumber: number, + ): Promise> { + const modelLoader = new MigratableModelLoader({ + urlResolver, + documentServiceFactory: getDocumentServiceFactory(id), + codeLoader: this.codeLoader, + generateCreateNewRequest: () => createLocalResolverCreateNewRequest(id), + }); + return modelLoader.loadExistingPaused(`${window.location.origin}/${id}`, sequenceNumber); + } +} diff --git a/examples/utils/migration-tools/src/modelLoader/migratableTinyliciousModelLoader.ts b/examples/utils/migration-tools/src/modelLoader/migratableTinyliciousModelLoader.ts new file mode 100644 index 000000000000..48f564c74892 --- /dev/null +++ b/examples/utils/migration-tools/src/modelLoader/migratableTinyliciousModelLoader.ts @@ -0,0 +1,70 @@ +/*! + * Copyright (c) Microsoft Corporation and contributors. All rights reserved. + * Licensed under the MIT License. + */ + +import { ICodeDetailsLoader } from "@fluidframework/container-definitions/internal"; +import type { + IDocumentServiceFactory, + IUrlResolver, +} from "@fluidframework/driver-definitions/internal"; +import { RouterliciousDocumentServiceFactory } from "@fluidframework/routerlicious-driver/internal"; +import { + InsecureTinyliciousTokenProvider, + InsecureTinyliciousUrlResolver, + createTinyliciousCreateNewRequest, +} from "@fluidframework/tinylicious-driver/internal"; + +import type { + IAttachedMigratableModel, + IDetachedMigratableModel, + IMigratableModelLoader, +} from "./interfaces.js"; +import { MigratableModelLoader } from "./migratableModelLoader.js"; + +class TinyliciousService { + public readonly documentServiceFactory: IDocumentServiceFactory; + public readonly urlResolver: IUrlResolver; + + constructor(tinyliciousPort?: number) { + const tokenProvider = new InsecureTinyliciousTokenProvider(); + this.urlResolver = new InsecureTinyliciousUrlResolver(tinyliciousPort); + this.documentServiceFactory = new RouterliciousDocumentServiceFactory(tokenProvider); + } +} + +/** + * @alpha + */ +export class MigratableTinyliciousModelLoader + implements IMigratableModelLoader +{ + private readonly tinyliciousService = new TinyliciousService(); + private readonly modelLoader: MigratableModelLoader; + + public constructor(codeLoader: ICodeDetailsLoader) { + this.modelLoader = new MigratableModelLoader({ + urlResolver: this.tinyliciousService.urlResolver, + documentServiceFactory: this.tinyliciousService.documentServiceFactory, + codeLoader, + generateCreateNewRequest: createTinyliciousCreateNewRequest, + }); + } + + public async supportsVersion(version: string): Promise { + return this.modelLoader.supportsVersion(version); + } + + public async createDetached(version: string): Promise> { + return this.modelLoader.createDetached(version); + } + public async loadExisting(id: string): Promise> { + return this.modelLoader.loadExisting(id); + } + public async loadExistingPaused( + id: string, + sequenceNumber: number, + ): Promise> { + return this.modelLoader.loadExistingPaused(id, sequenceNumber); + } +} diff --git a/examples/utils/migration-tools/tsconfig.cjs.json b/examples/utils/migration-tools/tsconfig.cjs.json new file mode 100644 index 000000000000..773d0806da58 --- /dev/null +++ b/examples/utils/migration-tools/tsconfig.cjs.json @@ -0,0 +1,7 @@ +{ + // This config must be used in a "type": "commonjs" environment. (Use fluid-tsc commonjs.) + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "./dist", + }, +} diff --git a/examples/utils/migration-tools/tsconfig.json b/examples/utils/migration-tools/tsconfig.json new file mode 100644 index 000000000000..92341d982223 --- /dev/null +++ b/examples/utils/migration-tools/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../../common/build/build-common/tsconfig.node16.json", + "include": ["src/**/*"], + "compilerOptions": { + "rootDir": "./src", + "outDir": "./lib", + "exactOptionalPropertyTypes": false, + }, +} diff --git a/examples/utils/migration-tools/tsdoc.json b/examples/utils/migration-tools/tsdoc.json new file mode 100644 index 000000000000..ecb918da5cb8 --- /dev/null +++ b/examples/utils/migration-tools/tsdoc.json @@ -0,0 +1,4 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/tsdoc/v0/tsdoc.schema.json", + "extends": ["../../../common/build/build-common/tsdoc-base.json"] +} diff --git a/examples/utils/webpack-fluid-loader/CHANGELOG.md b/examples/utils/webpack-fluid-loader/CHANGELOG.md index 780096cba622..6acf93ad70c3 100644 --- a/examples/utils/webpack-fluid-loader/CHANGELOG.md +++ b/examples/utils/webpack-fluid-loader/CHANGELOG.md @@ -1,5 +1,9 @@ # @fluid-example/webpack-fluid-loader +## 2.2.0 + +Dependency updates only. + ## 2.1.0 Dependency updates only. diff --git a/examples/utils/webpack-fluid-loader/package.json b/examples/utils/webpack-fluid-loader/package.json index fd715fbdb23a..fa0ec4c73496 100644 --- a/examples/utils/webpack-fluid-loader/package.json +++ b/examples/utils/webpack-fluid-loader/package.json @@ -1,6 +1,6 @@ { "name": "@fluid-example/webpack-fluid-loader", - "version": "2.2.0", + "version": "2.3.0", "private": true, "description": "Fluid object loader for webpack-dev-server", "homepage": "https://fluidframework.com", @@ -114,9 +114,9 @@ "@arethetypeswrong/cli": "^0.15.2", "@biomejs/biome": "~1.8.3", "@fluid-internal/mocha-test-setup": "workspace:~", - "@fluid-tools/build-cli": "0.43.0-285387", + "@fluid-tools/build-cli": "^0.44.0", "@fluidframework/build-common": "^2.0.3", - "@fluidframework/build-tools": "0.43.0-285387", + "@fluidframework/build-tools": "^0.44.0", "@fluidframework/eslint-config-fluid": "^5.3.0", "@types/express": "^4.17.21", "@types/fs-extra": "^9.0.11", diff --git a/examples/version-migration/live-schema-upgrade/CHANGELOG.md b/examples/version-migration/live-schema-upgrade/CHANGELOG.md index 6b8fd8bcd30b..da46c2b7c763 100644 --- a/examples/version-migration/live-schema-upgrade/CHANGELOG.md +++ b/examples/version-migration/live-schema-upgrade/CHANGELOG.md @@ -1,5 +1,9 @@ # @fluid-example/app-integration-live-schema-upgrade +## 2.2.0 + +Dependency updates only. + ## 2.1.0 Dependency updates only. diff --git a/examples/version-migration/live-schema-upgrade/package.json b/examples/version-migration/live-schema-upgrade/package.json index a16fa6a9b7d6..9fbbeb53971c 100644 --- a/examples/version-migration/live-schema-upgrade/package.json +++ b/examples/version-migration/live-schema-upgrade/package.json @@ -1,6 +1,6 @@ { "name": "@fluid-example/app-integration-live-schema-upgrade", - "version": "2.2.0", + "version": "2.3.0", "private": true, "description": "Example application that demonstrates how to add a data object to a live container.", "homepage": "https://fluidframework.com", @@ -53,9 +53,9 @@ }, "devDependencies": { "@biomejs/biome": "~1.8.3", - "@fluid-tools/build-cli": "0.43.0-285387", + "@fluid-tools/build-cli": "^0.44.0", "@fluidframework/build-common": "^2.0.3", - "@fluidframework/build-tools": "0.43.0-285387", + "@fluidframework/build-tools": "^0.44.0", "@fluidframework/eslint-config-fluid": "^5.3.0", "@fluidframework/test-tools": "^1.0.195075", "@types/jest": "29.5.3", diff --git a/examples/version-migration/same-container/CHANGELOG.md b/examples/version-migration/same-container/CHANGELOG.md index c0323366ae7f..3c6cdb045e84 100644 --- a/examples/version-migration/same-container/CHANGELOG.md +++ b/examples/version-migration/same-container/CHANGELOG.md @@ -1,5 +1,9 @@ # @fluid-example/version-migration-same-container +## 2.2.0 + +Dependency updates only. + ## 2.1.0 Dependency updates only. diff --git a/examples/version-migration/same-container/package.json b/examples/version-migration/same-container/package.json index 9164b0bc1571..a4c0cbf87b39 100644 --- a/examples/version-migration/same-container/package.json +++ b/examples/version-migration/same-container/package.json @@ -1,8 +1,8 @@ { "name": "@fluid-example/version-migration-same-container", - "version": "2.2.0", + "version": "2.3.0", "private": true, - "description": "Using external data to initialize the container state and serialize out afterwards.", + "description": "Migrate data between two formats by exporting and reimporting in the same container", "homepage": "https://fluidframework.com", "repository": { "type": "git", @@ -62,9 +62,9 @@ }, "devDependencies": { "@biomejs/biome": "~1.8.3", - "@fluid-tools/build-cli": "0.43.0-285387", + "@fluid-tools/build-cli": "^0.44.0", "@fluidframework/build-common": "^2.0.3", - "@fluidframework/build-tools": "0.43.0-285387", + "@fluidframework/build-tools": "^0.44.0", "@fluidframework/eslint-config-fluid": "^5.3.0", "@fluidframework/test-tools": "^1.0.195075", "@types/jest": "29.5.3", diff --git a/examples/version-migration/schema-upgrade/CHANGELOG.md b/examples/version-migration/schema-upgrade/CHANGELOG.md deleted file mode 100644 index 800826781285..000000000000 --- a/examples/version-migration/schema-upgrade/CHANGELOG.md +++ /dev/null @@ -1,97 +0,0 @@ -# @fluid-example/app-integration-schema-upgrade - -## 2.1.0 - -Dependency updates only. - -## 2.0.0-rc.5.0.0 - -Dependency updates only. - -## 2.0.0-rc.4.0.0 - -Dependency updates only. - -## 2.0.0-rc.3.0.0 - -Dependency updates only. - -## 2.0.0-rc.2.0.0 - -Dependency updates only. - -## 2.0.0-rc.1.0.0 - -Dependency updates only. - -## 2.0.0-internal.8.0.0 - -Dependency updates only. - -## 2.0.0-internal.7.4.0 - -Dependency updates only. - -## 2.0.0-internal.7.3.0 - -Dependency updates only. - -## 2.0.0-internal.7.2.0 - -Dependency updates only. - -## 2.0.0-internal.7.1.0 - -Dependency updates only. - -## 2.0.0-internal.7.0.0 - -Dependency updates only. - -## 2.0.0-internal.6.4.0 - -Dependency updates only. - -## 2.0.0-internal.6.3.0 - -Dependency updates only. - -## 2.0.0-internal.6.2.0 - -Dependency updates only. - -## 2.0.0-internal.6.1.0 - -Dependency updates only. - -## 2.0.0-internal.6.0.0 - -Dependency updates only. - -## 2.0.0-internal.5.4.0 - -Dependency updates only. - -## 2.0.0-internal.5.3.0 - -Dependency updates only. - -## 2.0.0-internal.5.2.0 - -Dependency updates only. - -## 2.0.0-internal.5.1.0 - -Dependency updates only. - -## 2.0.0-internal.5.0.0 - -Dependency updates only. - -## 2.0.0-internal.4.4.0 - -Dependency updates only. - -## 2.0.0-internal.4.1.0 - -Dependency updates only. diff --git a/examples/version-migration/schema-upgrade/src/modelVersion1/containerCode.ts b/examples/version-migration/schema-upgrade/src/modelVersion1/containerCode.ts deleted file mode 100644 index 07b7f101c591..000000000000 --- a/examples/version-migration/schema-upgrade/src/modelVersion1/containerCode.ts +++ /dev/null @@ -1,82 +0,0 @@ -/*! - * Copyright (c) Microsoft Corporation and contributors. All rights reserved. - * Licensed under the MIT License. - */ - -import type { IMigrationTool } from "@fluid-example/example-utils"; -import { - MigrationToolFactory, - ModelContainerRuntimeFactory, - getDataStoreEntryPoint, -} from "@fluid-example/example-utils"; -import type { IContainer } from "@fluidframework/container-definitions/internal"; -import type { IContainerRuntime } from "@fluidframework/container-runtime-definitions/internal"; - -import type { IInventoryList, IInventoryListAppModel } from "../modelInterfaces.js"; - -import { InventoryListAppModel } from "./appModel.js"; -import { InventoryListInstantiationFactory } from "./inventoryList.js"; - -const inventoryListId = "default-inventory-list"; -const migrationToolId = "migration-tool"; - -const migrationToolRegistryKey = "migration-tool"; -const migrationToolFactory = new MigrationToolFactory(); - -/** - * @internal - */ -export class InventoryListContainerRuntimeFactory extends ModelContainerRuntimeFactory { - /** - * Constructor for the factory. Supports a test mode which spawns the summarizer instantly. - * @param testMode - True to enable instant summarizer spawning. - */ - public constructor(testMode: boolean) { - super( - new Map([ - InventoryListInstantiationFactory.registryEntry, - [migrationToolRegistryKey, Promise.resolve(migrationToolFactory)], - ]), // registryEntries - testMode - ? { - summaryOptions: { - initialSummarizerDelayMs: 0, - }, - } - : undefined, - ); - } - - /** - * {@inheritDoc ModelContainerRuntimeFactory.containerInitializingFirstTime} - */ - protected async containerInitializingFirstTime(runtime: IContainerRuntime) { - const inventoryList = await runtime.createDataStore( - InventoryListInstantiationFactory.type, - ); - await inventoryList.trySetAlias(inventoryListId); - const migrationTool = await runtime.createDataStore(migrationToolRegistryKey); - await migrationTool.trySetAlias(migrationToolId); - } - - /** - * {@inheritDoc ModelContainerRuntimeFactory.containerHasInitialized} - */ - protected async containerHasInitialized(runtime: IContainerRuntime) { - // Force the MigrationTool to instantiate in all cases. The Quorum it uses must be loaded and running in - // order to respond with accept ops, and without this call the MigrationTool won't be instantiated on the - // summarizer client. - await getDataStoreEntryPoint(runtime, migrationToolId); - } - - /** - * {@inheritDoc ModelContainerRuntimeFactory.createModel} - */ - protected async createModel(runtime: IContainerRuntime, container: IContainer) { - return new InventoryListAppModel( - await getDataStoreEntryPoint(runtime, inventoryListId), - await getDataStoreEntryPoint(runtime, migrationToolId), - container, - ); - } -} diff --git a/examples/version-migration/schema-upgrade/src/modelVersion2/containerCode.ts b/examples/version-migration/schema-upgrade/src/modelVersion2/containerCode.ts deleted file mode 100644 index 53ccfef8d439..000000000000 --- a/examples/version-migration/schema-upgrade/src/modelVersion2/containerCode.ts +++ /dev/null @@ -1,79 +0,0 @@ -/*! - * Copyright (c) Microsoft Corporation and contributors. All rights reserved. - * Licensed under the MIT License. - */ - -import type { IMigrationTool } from "@fluid-example/example-utils"; -import { - MigrationToolFactory, - ModelContainerRuntimeFactory, - getDataStoreEntryPoint, -} from "@fluid-example/example-utils"; -import type { IContainer } from "@fluidframework/container-definitions/internal"; -import type { IContainerRuntime } from "@fluidframework/container-runtime-definitions/internal"; - -import type { IInventoryList, IInventoryListAppModel } from "../modelInterfaces.js"; - -import { InventoryListAppModel } from "./appModel.js"; -import { InventoryListInstantiationFactory } from "./inventoryList.js"; - -const inventoryListId = "default-inventory-list"; -const migrationToolId = "migration-tool"; - -const migrationToolRegistryKey = "migration-tool"; -const migrationToolFactory = new MigrationToolFactory(); - -export class InventoryListContainerRuntimeFactory extends ModelContainerRuntimeFactory { - /** - * Constructor for the factory. Supports a test mode which spawns the summarizer instantly. - * @param testMode - True to enable instant summarizer spawning. - */ - public constructor(testMode: boolean) { - super( - new Map([ - InventoryListInstantiationFactory.registryEntry, - [migrationToolRegistryKey, Promise.resolve(migrationToolFactory)], - ]), // registryEntries - testMode - ? { - summaryOptions: { - initialSummarizerDelayMs: 0, - }, - } - : undefined, - ); - } - - /** - * {@inheritDoc ModelContainerRuntimeFactory.containerInitializingFirstTime} - */ - protected async containerInitializingFirstTime(runtime: IContainerRuntime) { - const inventoryList = await runtime.createDataStore( - InventoryListInstantiationFactory.type, - ); - await inventoryList.trySetAlias(inventoryListId); - const migrationTool = await runtime.createDataStore(migrationToolRegistryKey); - await migrationTool.trySetAlias(migrationToolId); - } - - /** - * {@inheritDoc ModelContainerRuntimeFactory.containerHasInitialized} - */ - protected async containerHasInitialized(runtime: IContainerRuntime) { - // Force the MigrationTool to instantiate in all cases. The Quorum it uses must be loaded and running in - // order to respond with accept ops, and without this call the MigrationTool won't be instantiated on the - // summarizer client. - await getDataStoreEntryPoint(runtime, migrationToolId); - } - - /** - * {@inheritDoc ModelContainerRuntimeFactory.createModel} - */ - protected async createModel(runtime: IContainerRuntime, container: IContainer) { - return new InventoryListAppModel( - await getDataStoreEntryPoint(runtime, inventoryListId), - await getDataStoreEntryPoint(runtime, migrationToolId), - container, - ); - } -} diff --git a/examples/version-migration/separate-container/.eslintrc.cjs b/examples/version-migration/separate-container/.eslintrc.cjs new file mode 100644 index 000000000000..484c63b7e874 --- /dev/null +++ b/examples/version-migration/separate-container/.eslintrc.cjs @@ -0,0 +1,9 @@ +/*! + * Copyright (c) Microsoft Corporation and contributors. All rights reserved. + * Licensed under the MIT License. + */ + +module.exports = { + extends: [require.resolve("@fluidframework/eslint-config-fluid"), "prettier"], + rules: {}, +}; diff --git a/examples/version-migration/schema-upgrade/.gitignore b/examples/version-migration/separate-container/.gitignore similarity index 100% rename from examples/version-migration/schema-upgrade/.gitignore rename to examples/version-migration/separate-container/.gitignore diff --git a/examples/version-migration/separate-container/.npmignore b/examples/version-migration/separate-container/.npmignore new file mode 100644 index 000000000000..f518002fc4dd --- /dev/null +++ b/examples/version-migration/separate-container/.npmignore @@ -0,0 +1,7 @@ +nyc +*.log +**/*.tsbuildinfo +src/test +dist/test +lib/test +**/_api-extractor-temp/** diff --git a/examples/version-migration/separate-container/LICENSE b/examples/version-migration/separate-container/LICENSE new file mode 100644 index 000000000000..60af0a6a40e9 --- /dev/null +++ b/examples/version-migration/separate-container/LICENSE @@ -0,0 +1,21 @@ +Copyright (c) Microsoft Corporation and contributors. All rights reserved. + +MIT License + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/examples/version-migration/schema-upgrade/README.md b/examples/version-migration/separate-container/README.md similarity index 98% rename from examples/version-migration/schema-upgrade/README.md rename to examples/version-migration/separate-container/README.md index 620ceb45ca01..af1d875bfa00 100644 --- a/examples/version-migration/schema-upgrade/README.md +++ b/examples/version-migration/separate-container/README.md @@ -1,4 +1,4 @@ -# @fluid-example/app-integration-schema-upgrade +# @fluid-example/version-migration-separate-container This example experiments with an approach for migrating data from an existing Fluid container into a new Fluid container which may have a different schema or code running on it. @@ -76,7 +76,7 @@ You can run this example using the following steps: 1. Enable [corepack](https://nodejs.org/docs/latest-v16.x/api/corepack.html) by running `corepack enable`. 1. Run `pnpm install` and `pnpm run build:fast --nolint` from the `FluidFramework` root directory. - For an even faster build, you can add the package name to the build command, like this: - `pnpm run build:fast --nolint @fluid-example/app-integration-schema-upgrade` + `pnpm run build:fast --nolint @fluid-example/version-migration-separate-container` 1. In a separate terminal, start a Tinylicious server by following the instructions in [Tinylicious](https://github.com/microsoft/FluidFramework/tree/main/server/routerlicious/packages/tinylicious). 1. Run `pnpm start` from this directory and open in a web browser to see the app running. diff --git a/examples/version-migration/separate-container/biome.jsonc b/examples/version-migration/separate-container/biome.jsonc new file mode 100644 index 000000000000..4b65e1c0aea2 --- /dev/null +++ b/examples/version-migration/separate-container/biome.jsonc @@ -0,0 +1,4 @@ +{ + "$schema": "./node_modules/@biomejs/biome/configuration_schema.json", + "extends": ["../../../biome.jsonc"] +} diff --git a/examples/version-migration/schema-upgrade/jest-puppeteer.config.cjs b/examples/version-migration/separate-container/jest-puppeteer.config.cjs similarity index 100% rename from examples/version-migration/schema-upgrade/jest-puppeteer.config.cjs rename to examples/version-migration/separate-container/jest-puppeteer.config.cjs diff --git a/examples/version-migration/schema-upgrade/jest.config.cjs b/examples/version-migration/separate-container/jest.config.cjs similarity index 100% rename from examples/version-migration/schema-upgrade/jest.config.cjs rename to examples/version-migration/separate-container/jest.config.cjs diff --git a/examples/version-migration/schema-upgrade/package.json b/examples/version-migration/separate-container/package.json similarity index 89% rename from examples/version-migration/schema-upgrade/package.json rename to examples/version-migration/separate-container/package.json index 36ad6ff89541..557230063989 100644 --- a/examples/version-migration/schema-upgrade/package.json +++ b/examples/version-migration/separate-container/package.json @@ -1,13 +1,13 @@ { - "name": "@fluid-example/app-integration-schema-upgrade", - "version": "2.2.0", + "name": "@fluid-example/version-migration-separate-container", + "version": "2.3.0", "private": true, - "description": "Using external data to initialize the container state and serialize out afterwards.", + "description": "Migrate data between two formats by exporting and reimporting in a new container", "homepage": "https://fluidframework.com", "repository": { "type": "git", "url": "https://github.com/microsoft/FluidFramework.git", - "directory": "examples/version-migration/schema-upgrade" + "directory": "examples/version-migration/separate-container" }, "license": "MIT", "author": "Microsoft and contributors", @@ -38,11 +38,13 @@ }, "dependencies": { "@fluid-example/example-utils": "workspace:~", + "@fluid-example/migration-tools": "workspace:~", "@fluid-internal/client-utils": "workspace:~", "@fluidframework/aqueduct": "workspace:~", "@fluidframework/cell": "workspace:~", "@fluidframework/container-definitions": "workspace:~", "@fluidframework/container-loader": "workspace:~", + "@fluidframework/container-runtime": "workspace:~", "@fluidframework/container-runtime-definitions": "workspace:~", "@fluidframework/core-interfaces": "workspace:~", "@fluidframework/driver-definitions": "workspace:~", @@ -62,9 +64,9 @@ }, "devDependencies": { "@biomejs/biome": "~1.8.3", - "@fluid-tools/build-cli": "0.43.0-285387", + "@fluid-tools/build-cli": "^0.44.0", "@fluidframework/build-common": "^2.0.3", - "@fluidframework/build-tools": "0.43.0-285387", + "@fluidframework/build-tools": "^0.44.0", "@fluidframework/eslint-config-fluid": "^5.3.0", "@fluidframework/test-tools": "^1.0.195075", "@types/jest": "29.5.3", diff --git a/examples/version-migration/separate-container/prettier.config.cjs b/examples/version-migration/separate-container/prettier.config.cjs new file mode 100644 index 000000000000..d4870022599f --- /dev/null +++ b/examples/version-migration/separate-container/prettier.config.cjs @@ -0,0 +1,8 @@ +/*! + * Copyright (c) Microsoft Corporation and contributors. All rights reserved. + * Licensed under the MIT License. + */ + +module.exports = { + ...require("@fluidframework/build-common/prettier.config.cjs"), +}; diff --git a/examples/version-migration/schema-upgrade/src/dataTransform.ts b/examples/version-migration/separate-container/src/dataTransform.ts similarity index 84% rename from examples/version-migration/schema-upgrade/src/dataTransform.ts rename to examples/version-migration/separate-container/src/dataTransform.ts index f8874ef7ba18..f08fb8ceb45e 100644 --- a/examples/version-migration/schema-upgrade/src/dataTransform.ts +++ b/examples/version-migration/separate-container/src/dataTransform.ts @@ -3,7 +3,13 @@ * Licensed under the MIT License. */ -import type { DataTransformationCallback } from "@fluid-example/example-utils"; +// eslint-disable-next-line import/no-internal-modules +import type { DataTransformationCallback } from "@fluid-example/migration-tools/internal"; + +export interface IParsedInventoryItemData { + name: string; + quantity: number; +} /** * Read the version of the string data, to understand how to parse it. This is shared between versions. @@ -12,7 +18,7 @@ import type { DataTransformationCallback } from "@fluid-example/example-utils"; * @param stringData - The string data to examine * @returns The version string */ -export function readVersion(stringData: string) { +export function readVersion(stringData: string): string { const lines = stringData.split("\n"); const [versionTag, version] = lines[0].split(":"); if (versionTag !== "version" || typeof version !== "string" || version === "") { @@ -27,7 +33,7 @@ export function readVersion(stringData: string) { * @param stringData - version:one formatted string data * @returns An array of objects, each representing a single inventory item */ -export function parseStringDataVersionOne(stringData: string) { +export function parseStringDataVersionOne(stringData: string): IParsedInventoryItemData[] { const version = readVersion(stringData); if (version !== "one") { throw new Error(`Expected to parse version one, got version ${version}`); @@ -42,7 +48,7 @@ export function parseStringDataVersionOne(stringData: string) { return itemStrings.map((itemString) => { const [itemNameString, itemQuantityString] = itemString.split(":"); - return { name: itemNameString, quantity: parseInt(itemQuantityString, 10) }; + return { name: itemNameString, quantity: Number.parseInt(itemQuantityString, 10) }; }); } @@ -52,7 +58,7 @@ export function parseStringDataVersionOne(stringData: string) { * @param stringData - version:two formatted string data * @returns An array of objects, each representing a single inventory item */ -export function parseStringDataVersionTwo(stringData: string) { +export function parseStringDataVersionTwo(stringData: string): IParsedInventoryItemData[] { const version = readVersion(stringData); if (version !== "two") { throw new Error(`Expected to parse version two, got version ${version}`); @@ -67,11 +73,11 @@ export function parseStringDataVersionTwo(stringData: string) { return itemStrings.map((itemString) => { const [itemNameString, itemQuantityString] = itemString.split("\t"); - return { name: itemNameString, quantity: parseInt(itemQuantityString, 10) }; + return { name: itemNameString, quantity: Number.parseInt(itemQuantityString, 10) }; }); } -function parseStringData(stringData: string) { +function parseStringData(stringData: string): IParsedInventoryItemData[] { const version = readVersion(stringData); if (version === "one") { return parseStringDataVersionOne(stringData); @@ -82,7 +88,7 @@ function parseStringData(stringData: string) { } } -function transformToOne(stringData: string) { +function transformToOne(stringData: string): string { const inventoryItems = parseStringData(stringData); const inventoryItemStrings = inventoryItems.map((inventoryItem) => { return `${inventoryItem.name}:${inventoryItem.quantity.toString()}`; @@ -90,7 +96,7 @@ function transformToOne(stringData: string) { return `version:one\n${inventoryItemStrings.join("\n")}`; } -function transformToTwo(stringData: string) { +function transformToTwo(stringData: string): string { const inventoryItems = parseStringData(stringData); const inventoryItemStrings = inventoryItems.map((inventoryItem) => { return `${inventoryItem.name}\t${inventoryItem.quantity.toString()}`; diff --git a/examples/version-migration/schema-upgrade/src/demoCodeLoader.ts b/examples/version-migration/separate-container/src/demoCodeLoader.ts similarity index 97% rename from examples/version-migration/schema-upgrade/src/demoCodeLoader.ts rename to examples/version-migration/separate-container/src/demoCodeLoader.ts index 8f57a27e382a..5f5598049ed1 100644 --- a/examples/version-migration/schema-upgrade/src/demoCodeLoader.ts +++ b/examples/version-migration/separate-container/src/demoCodeLoader.ts @@ -45,12 +45,15 @@ export class DemoCodeLoader implements ICodeDetailsLoader { }; switch (version) { - case "one": + case "one": { return v1ModuleWithDetails; - case "two": + } + case "two": { return v2ModuleWithDetails; - default: + } + default: { throw new Error("Unknown version"); + } } } } diff --git a/examples/version-migration/schema-upgrade/src/index.html b/examples/version-migration/separate-container/src/index.html similarity index 100% rename from examples/version-migration/schema-upgrade/src/index.html rename to examples/version-migration/separate-container/src/index.html diff --git a/examples/version-migration/schema-upgrade/src/modelInterfaces.ts b/examples/version-migration/separate-container/src/modelInterfaces.ts similarity index 100% rename from examples/version-migration/schema-upgrade/src/modelInterfaces.ts rename to examples/version-migration/separate-container/src/modelInterfaces.ts diff --git a/examples/version-migration/schema-upgrade/src/modelVersion1/appModel.ts b/examples/version-migration/separate-container/src/modelVersion1/appModel.ts similarity index 93% rename from examples/version-migration/schema-upgrade/src/modelVersion1/appModel.ts rename to examples/version-migration/separate-container/src/modelVersion1/appModel.ts index 471836a4c509..efceedc02c57 100644 --- a/examples/version-migration/schema-upgrade/src/modelVersion1/appModel.ts +++ b/examples/version-migration/separate-container/src/modelVersion1/appModel.ts @@ -3,7 +3,8 @@ * Licensed under the MIT License. */ -import type { IMigratableModel, IMigrationTool } from "@fluid-example/example-utils"; +// eslint-disable-next-line import/no-internal-modules +import type { IMigratableModel } from "@fluid-example/migration-tools/internal"; import { AttachState } from "@fluidframework/container-definitions"; import { IContainer } from "@fluidframework/container-definitions/internal"; @@ -25,7 +26,6 @@ export class InventoryListAppModel implements IInventoryListAppModel, IMigratabl public constructor( public readonly inventoryList: IInventoryList, - public readonly migrationTool: IMigrationTool, private readonly container: IContainer, ) {} @@ -61,7 +61,7 @@ export class InventoryListAppModel implements IInventoryListAppModel, IMigratabl return `version:one\n${inventoryItemStrings.join("\n")}`; }; - public close() { - this.container.close(); + public dispose(): void { + this.container.dispose(); } } diff --git a/examples/version-migration/separate-container/src/modelVersion1/containerCode.ts b/examples/version-migration/separate-container/src/modelVersion1/containerCode.ts new file mode 100644 index 000000000000..a0ab620be2e6 --- /dev/null +++ b/examples/version-migration/separate-container/src/modelVersion1/containerCode.ts @@ -0,0 +1,91 @@ +/*! + * Copyright (c) Microsoft Corporation and contributors. All rights reserved. + * Licensed under the MIT License. + */ + +import { getDataStoreEntryPoint } from "@fluid-example/example-utils"; +import { + type IMigratableModel, + instantiateMigratableRuntime, + // eslint-disable-next-line import/no-internal-modules +} from "@fluid-example/migration-tools/internal"; +import type { + IContainer, + IContainerContext, + IRuntime, + IRuntimeFactory, +} from "@fluidframework/container-definitions/internal"; +import type { IContainerRuntimeOptions } from "@fluidframework/container-runtime/internal"; +import type { IContainerRuntime } from "@fluidframework/container-runtime-definitions/internal"; + +import type { IInventoryList, IInventoryListAppModel } from "../modelInterfaces.js"; + +import { InventoryListAppModel } from "./appModel.js"; +import { InventoryListInstantiationFactory } from "./inventoryList.js"; + +const inventoryListId = "default-inventory-list"; + +/** + * @internal + */ +export class InventoryListContainerRuntimeFactory implements IRuntimeFactory { + public get IRuntimeFactory(): IRuntimeFactory { + return this; + } + + private readonly registryEntries = new Map([ + InventoryListInstantiationFactory.registryEntry, + ]); + private readonly runtimeOptions: IContainerRuntimeOptions | undefined; + /** + * Constructor for the factory. Supports a test mode which spawns the summarizer instantly. + * @param testMode - True to enable instant summarizer spawning. + */ + public constructor(testMode: boolean) { + this.runtimeOptions = testMode + ? { + summaryOptions: { + initialSummarizerDelayMs: 0, + }, + } + : undefined; + } + + public async instantiateRuntime( + context: IContainerContext, + existing: boolean, + ): Promise { + const runtime = await instantiateMigratableRuntime( + context, + existing, + this.registryEntries, + this.createModel, + this.runtimeOptions, + ); + + if (!existing) { + await this.containerInitializingFirstTime(runtime); + } + + return runtime; + } + + private readonly containerInitializingFirstTime = async ( + runtime: IContainerRuntime, + ): Promise => { + const inventoryList = await runtime.createDataStore( + InventoryListInstantiationFactory.type, + ); + await inventoryList.trySetAlias(inventoryListId); + }; + + private readonly createModel = async ( + runtime: IContainerRuntime, + container: IContainer, + ): Promise => { + return new InventoryListAppModel( + await getDataStoreEntryPoint(runtime, inventoryListId), + container, + ); + }; +} diff --git a/examples/version-migration/schema-upgrade/src/modelVersion1/index.ts b/examples/version-migration/separate-container/src/modelVersion1/index.ts similarity index 100% rename from examples/version-migration/schema-upgrade/src/modelVersion1/index.ts rename to examples/version-migration/separate-container/src/modelVersion1/index.ts diff --git a/examples/version-migration/schema-upgrade/src/modelVersion1/inventoryList.ts b/examples/version-migration/separate-container/src/modelVersion1/inventoryList.ts similarity index 73% rename from examples/version-migration/schema-upgrade/src/modelVersion1/inventoryList.ts rename to examples/version-migration/separate-container/src/modelVersion1/inventoryList.ts index 67861fda85ea..2806632ff0d4 100644 --- a/examples/version-migration/schema-upgrade/src/modelVersion1/inventoryList.ts +++ b/examples/version-migration/separate-container/src/modelVersion1/inventoryList.ts @@ -5,21 +5,27 @@ import { EventEmitter } from "@fluid-example/example-utils"; import { DataObject, DataObjectFactory } from "@fluidframework/aqueduct/internal"; -import { SharedCell, type ISharedCell } from "@fluidframework/cell/internal"; -import { SharedString } from "@fluidframework/sequence/internal"; +import { type ISharedCell, SharedCell } from "@fluidframework/cell/internal"; +import type { IFluidHandle } from "@fluidframework/core-interfaces"; +import { type ISharedString, SharedString } from "@fluidframework/sequence/internal"; import { v4 as uuid } from "uuid"; import type { IInventoryItem, IInventoryList } from "../modelInterfaces.js"; +interface IStoredInventoryItem { + name: IFluidHandle; + quantity: IFluidHandle>; +} + class InventoryItem extends EventEmitter implements IInventoryItem { - public get id() { + public get id(): string { return this._id; } // Probably would be nice to not hand out the SharedString, but the CollaborativeInput expects it. - public get name() { + public get name(): ISharedString { return this._name; } - public get quantity() { + public get quantity(): number { const cellValue = this._quantity.get(); if (cellValue === undefined) { throw new Error("Expected a valid quantity"); @@ -31,7 +37,7 @@ class InventoryItem extends EventEmitter implements IInventoryItem { } public constructor( private readonly _id: string, - private readonly _name: SharedString, + private readonly _name: ISharedString, private readonly _quantity: ISharedCell, ) { super(); @@ -53,29 +59,34 @@ class InventoryItem extends EventEmitter implements IInventoryItem { export class InventoryList extends DataObject implements IInventoryList { private readonly inventoryItems = new Map(); - public readonly addItem = (name: string, quantity: number) => { + public readonly addItem = (name: string, quantity: number): void => { const nameString = SharedString.create(this.runtime); nameString.insertText(0, name); - const quantityCell: ISharedCell = SharedCell.create(this.runtime); + const quantityCell: ISharedCell = SharedCell.create( + this.runtime, + ) as ISharedCell; quantityCell.set(quantity); const id = uuid(); this.root.set(id, { name: nameString.handle, quantity: quantityCell.handle }); }; - public readonly deleteItem = (id: string) => { + public readonly deleteItem = (id: string): void => { this.root.delete(id); }; - public readonly getItems = () => { + public readonly getItems = (): IInventoryItem[] => { return [...this.inventoryItems.values()]; }; - public readonly getItem = (id: string) => { + public readonly getItem = (id: string): IInventoryItem | undefined => { return this.inventoryItems.get(id); }; - private readonly handleItemAdded = async (id: string) => { - const itemData = this.root.get(id); + private readonly handleItemAdded = async (id: string): Promise => { + // We expect this stored inventory item must exist because this handler is run in response to + // the given id being the subject of a valueChanged event. + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const itemData = this.root.get(id)!; const [nameSharedString, quantitySharedCell] = await Promise.all([ itemData.name.get(), itemData.quantity.get(), @@ -89,7 +100,7 @@ export class InventoryList extends DataObject implements IInventoryList { this.emit("itemAdded", newInventoryItem); }; - private readonly handleItemDeleted = (id: string) => { + private readonly handleItemDeleted = (id: string): void => { const deletedItem = this.inventoryItems.get(id); this.inventoryItems.delete(id); this.emit("itemDeleted", deletedItem); @@ -99,7 +110,7 @@ export class InventoryList extends DataObject implements IInventoryList { * hasInitialized is run by each client as they load the DataObject. Here we use it to set up usage of the * DataObject, by registering an event listener for changes to the inventory list. */ - protected async hasInitialized() { + protected async hasInitialized(): Promise { this.root.on("valueChanged", (changed) => { if (changed.previousValue === undefined) { // Must be from adding a new item @@ -116,7 +127,9 @@ export class InventoryList extends DataObject implements IInventoryList { } }); - for (const [id, itemData] of this.root) { + for (const [id, itemData] of this.root.entries() as IterableIterator< + [string, IStoredInventoryItem] + >) { const [nameSharedString, quantitySharedCell] = await Promise.all([ itemData.name.get(), itemData.quantity.get(), diff --git a/examples/version-migration/schema-upgrade/src/modelVersion2/appModel.ts b/examples/version-migration/separate-container/src/modelVersion2/appModel.ts similarity index 93% rename from examples/version-migration/schema-upgrade/src/modelVersion2/appModel.ts rename to examples/version-migration/separate-container/src/modelVersion2/appModel.ts index 56e9af52dd3b..d38e63928096 100644 --- a/examples/version-migration/schema-upgrade/src/modelVersion2/appModel.ts +++ b/examples/version-migration/separate-container/src/modelVersion2/appModel.ts @@ -3,7 +3,8 @@ * Licensed under the MIT License. */ -import type { IMigratableModel, IMigrationTool } from "@fluid-example/example-utils"; +// eslint-disable-next-line import/no-internal-modules +import type { IMigratableModel } from "@fluid-example/migration-tools/internal"; import { AttachState } from "@fluidframework/container-definitions"; import { IContainer } from "@fluidframework/container-definitions/internal"; @@ -25,7 +26,6 @@ export class InventoryListAppModel implements IInventoryListAppModel, IMigratabl public constructor( public readonly inventoryList: IInventoryList, - public readonly migrationTool: IMigrationTool, private readonly container: IContainer, ) {} @@ -61,7 +61,7 @@ export class InventoryListAppModel implements IInventoryListAppModel, IMigratabl return `version:two\n${inventoryItemStrings.join("\n")}`; }; - public close() { - this.container.close(); + public dispose(): void { + this.container.dispose(); } } diff --git a/examples/version-migration/separate-container/src/modelVersion2/containerCode.ts b/examples/version-migration/separate-container/src/modelVersion2/containerCode.ts new file mode 100644 index 000000000000..a0ab620be2e6 --- /dev/null +++ b/examples/version-migration/separate-container/src/modelVersion2/containerCode.ts @@ -0,0 +1,91 @@ +/*! + * Copyright (c) Microsoft Corporation and contributors. All rights reserved. + * Licensed under the MIT License. + */ + +import { getDataStoreEntryPoint } from "@fluid-example/example-utils"; +import { + type IMigratableModel, + instantiateMigratableRuntime, + // eslint-disable-next-line import/no-internal-modules +} from "@fluid-example/migration-tools/internal"; +import type { + IContainer, + IContainerContext, + IRuntime, + IRuntimeFactory, +} from "@fluidframework/container-definitions/internal"; +import type { IContainerRuntimeOptions } from "@fluidframework/container-runtime/internal"; +import type { IContainerRuntime } from "@fluidframework/container-runtime-definitions/internal"; + +import type { IInventoryList, IInventoryListAppModel } from "../modelInterfaces.js"; + +import { InventoryListAppModel } from "./appModel.js"; +import { InventoryListInstantiationFactory } from "./inventoryList.js"; + +const inventoryListId = "default-inventory-list"; + +/** + * @internal + */ +export class InventoryListContainerRuntimeFactory implements IRuntimeFactory { + public get IRuntimeFactory(): IRuntimeFactory { + return this; + } + + private readonly registryEntries = new Map([ + InventoryListInstantiationFactory.registryEntry, + ]); + private readonly runtimeOptions: IContainerRuntimeOptions | undefined; + /** + * Constructor for the factory. Supports a test mode which spawns the summarizer instantly. + * @param testMode - True to enable instant summarizer spawning. + */ + public constructor(testMode: boolean) { + this.runtimeOptions = testMode + ? { + summaryOptions: { + initialSummarizerDelayMs: 0, + }, + } + : undefined; + } + + public async instantiateRuntime( + context: IContainerContext, + existing: boolean, + ): Promise { + const runtime = await instantiateMigratableRuntime( + context, + existing, + this.registryEntries, + this.createModel, + this.runtimeOptions, + ); + + if (!existing) { + await this.containerInitializingFirstTime(runtime); + } + + return runtime; + } + + private readonly containerInitializingFirstTime = async ( + runtime: IContainerRuntime, + ): Promise => { + const inventoryList = await runtime.createDataStore( + InventoryListInstantiationFactory.type, + ); + await inventoryList.trySetAlias(inventoryListId); + }; + + private readonly createModel = async ( + runtime: IContainerRuntime, + container: IContainer, + ): Promise => { + return new InventoryListAppModel( + await getDataStoreEntryPoint(runtime, inventoryListId), + container, + ); + }; +} diff --git a/examples/version-migration/schema-upgrade/src/modelVersion2/index.ts b/examples/version-migration/separate-container/src/modelVersion2/index.ts similarity index 100% rename from examples/version-migration/schema-upgrade/src/modelVersion2/index.ts rename to examples/version-migration/separate-container/src/modelVersion2/index.ts diff --git a/examples/version-migration/schema-upgrade/src/modelVersion2/inventoryList.ts b/examples/version-migration/separate-container/src/modelVersion2/inventoryList.ts similarity index 76% rename from examples/version-migration/schema-upgrade/src/modelVersion2/inventoryList.ts rename to examples/version-migration/separate-container/src/modelVersion2/inventoryList.ts index 1294c80f9f97..708c2c4fa868 100644 --- a/examples/version-migration/schema-upgrade/src/modelVersion2/inventoryList.ts +++ b/examples/version-migration/separate-container/src/modelVersion2/inventoryList.ts @@ -5,23 +5,29 @@ import { EventEmitter } from "@fluid-example/example-utils"; import { DataObject, DataObjectFactory } from "@fluidframework/aqueduct/internal"; +import type { IFluidHandle } from "@fluidframework/core-interfaces"; import { type ISharedMap, SharedMap } from "@fluidframework/map/internal"; -import { SharedString } from "@fluidframework/sequence/internal"; +import { type ISharedString, SharedString } from "@fluidframework/sequence/internal"; import { v4 as uuid } from "uuid"; import type { IInventoryItem, IInventoryList } from "../modelInterfaces.js"; const quantityKey = "quantity"; +interface IStoredInventoryItem { + name: IFluidHandle; + quantity: IFluidHandle; +} + class InventoryItem extends EventEmitter implements IInventoryItem { - public get id() { + public get id(): string { return this._id; } // Probably would be nice to not hand out the SharedString, but the CollaborativeInput expects it. - public get name() { + public get name(): ISharedString { return this._name; } - public get quantity() { + public get quantity(): number { const mapValue = this._quantity.get(quantityKey); if (mapValue === undefined) { throw new Error("Expected a valid quantity"); @@ -33,7 +39,7 @@ class InventoryItem extends EventEmitter implements IInventoryItem { } public constructor( private readonly _id: string, - private readonly _name: SharedString, + private readonly _name: ISharedString, private readonly _quantity: ISharedMap, ) { super(); @@ -54,7 +60,7 @@ class InventoryItem extends EventEmitter implements IInventoryItem { export class InventoryList extends DataObject implements IInventoryList { private readonly inventoryItems = new Map(); - public readonly addItem = (name: string, quantity: number) => { + public readonly addItem = (name: string, quantity: number): void => { const nameString = SharedString.create(this.runtime); nameString.insertText(0, name); const quantityMap: SharedMap = SharedMap.create(this.runtime); @@ -63,20 +69,23 @@ export class InventoryList extends DataObject implements IInventoryList { this.root.set(id, { name: nameString.handle, quantity: quantityMap.handle }); }; - public readonly deleteItem = (id: string) => { + public readonly deleteItem = (id: string): void => { this.root.delete(id); }; - public readonly getItems = () => { + public readonly getItems = (): IInventoryItem[] => { return [...this.inventoryItems.values()]; }; - public readonly getItem = (id: string) => { + public readonly getItem = (id: string): IInventoryItem | undefined => { return this.inventoryItems.get(id); }; - private readonly handleItemAdded = async (id: string) => { - const itemData = this.root.get(id); + private readonly handleItemAdded = async (id: string): Promise => { + // We expect this stored inventory item must exist because this handler is run in response to + // the given id being the subject of a valueChanged event. + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const itemData = this.root.get(id)!; const [nameSharedString, quantitySharedMap] = await Promise.all([ itemData.name.get(), itemData.quantity.get(), @@ -90,7 +99,7 @@ export class InventoryList extends DataObject implements IInventoryList { this.emit("itemAdded", newInventoryItem); }; - private readonly handleItemDeleted = (id: string) => { + private readonly handleItemDeleted = (id: string): void => { const deletedItem = this.inventoryItems.get(id); this.inventoryItems.delete(id); this.emit("itemDeleted", deletedItem); @@ -100,7 +109,7 @@ export class InventoryList extends DataObject implements IInventoryList { * hasInitialized is run by each client as they load the DataObject. Here we use it to set up usage of the * DataObject, by registering an event listener for changes to the inventory list. */ - protected async hasInitialized() { + protected async hasInitialized(): Promise { this.root.on("valueChanged", (changed) => { if (changed.previousValue === undefined) { // Must be from adding a new item @@ -117,7 +126,9 @@ export class InventoryList extends DataObject implements IInventoryList { } }); - for (const [id, itemData] of this.root) { + for (const [id, itemData] of this.root.entries() as IterableIterator< + [string, IStoredInventoryItem] + >) { const [nameSharedString, quantitySharedMap] = await Promise.all([ itemData.name.get(), itemData.quantity.get(), diff --git a/examples/version-migration/schema-upgrade/src/start.ts b/examples/version-migration/separate-container/src/start.ts similarity index 74% rename from examples/version-migration/schema-upgrade/src/start.ts rename to examples/version-migration/separate-container/src/start.ts index 90c9256fc352..a8f3632752a5 100644 --- a/examples/version-migration/schema-upgrade/src/start.ts +++ b/examples/version-migration/separate-container/src/start.ts @@ -3,23 +3,29 @@ * Licensed under the MIT License. */ -import type { IMigratableModel, IVersionedModel } from "@fluid-example/example-utils"; -import { Migrator, ModelLoader } from "@fluid-example/example-utils"; +import type { + IMigratableModel, + IMigrationTool, + IVersionedModel, + // eslint-disable-next-line import/no-internal-modules +} from "@fluid-example/migration-tools/internal"; +// eslint-disable-next-line import/no-internal-modules +import { MigratableModelLoader, Migrator } from "@fluid-example/migration-tools/internal"; import { RouterliciousDocumentServiceFactory } from "@fluidframework/routerlicious-driver/internal"; import { InsecureTinyliciousTokenProvider, InsecureTinyliciousUrlResolver, createTinyliciousCreateNewRequest, } from "@fluidframework/tinylicious-driver/internal"; -import React from "react"; -import ReactDOM from "react-dom"; +import { createElement } from "react"; +import { render, unmountComponentAtNode } from "react-dom"; import { inventoryListDataTransformationCallback } from "./dataTransform.js"; import { DemoCodeLoader } from "./demoCodeLoader.js"; import type { IInventoryListAppModel } from "./modelInterfaces.js"; import { DebugView, InventoryListAppView } from "./view/index.js"; -const updateTabForId = (id: string) => { +const updateTabForId = (id: string): void => { // Update the URL with the actual ID location.hash = id; @@ -33,23 +39,24 @@ const isIInventoryListAppModel = ( return model.version === "one" || model.version === "two"; }; -const getUrlForContainerId = (containerId: string) => `/#${containerId}`; +const getUrlForContainerId = (containerId: string): string => `/#${containerId}`; -const render = (model: IVersionedModel) => { - const appDiv = document.getElementById("app") as HTMLDivElement; - ReactDOM.unmountComponentAtNode(appDiv); +const renderModel = (model: IVersionedModel, migrationTool: IMigrationTool): void => { + const appDiv = document.querySelector("#app") as HTMLDivElement; + unmountComponentAtNode(appDiv); // This demo uses the same view for both versions 1 & 2 - if we wanted to use different views for different model // versions, we could check its version here and select the appropriate view. Or we could even write ourselves a // view code loader to pull in the view dynamically based on the version we discover. if (isIInventoryListAppModel(model)) { - ReactDOM.render(React.createElement(InventoryListAppView, { model }), appDiv); + render(createElement(InventoryListAppView, { model, migrationTool }), appDiv); // The DebugView is just for demo purposes, to manually control code proposal and inspect the state. - const debugDiv = document.getElementById("debug") as HTMLDivElement; - ReactDOM.unmountComponentAtNode(debugDiv); - ReactDOM.render( - React.createElement(DebugView, { + const debugDiv = document.querySelector("#debug") as HTMLDivElement; + unmountComponentAtNode(debugDiv); + render( + createElement(DebugView, { model, + migrationTool, getUrlForContainerId, }), debugDiv, @@ -66,7 +73,7 @@ async function start(): Promise { // container.getEntryPoint().model if we knew that was the model. // TODO: This is really loading an IInventoryListAppModel & IMigratableModel (we know this because of what the // DemoCodeLoader supports). Should we just use that more-specific type in the typing here? - const modelLoader = new ModelLoader({ + const modelLoader = new MigratableModelLoader({ urlResolver: new InsecureTinyliciousUrlResolver(), documentServiceFactory: new RouterliciousDocumentServiceFactory( new InsecureTinyliciousTokenProvider(), @@ -77,32 +84,40 @@ async function start(): Promise { let id: string; let model: IMigratableModel; + let migrationTool: IMigrationTool; if (location.hash.length === 0) { // Choosing to create with the "old" version for demo purposes, so we can demo the upgrade flow. // Normally we would create with the most-recent version. const createResponse = await modelLoader.createDetached("one"); model = createResponse.model; + migrationTool = createResponse.migrationTool; id = await createResponse.attach(); } else { - id = location.hash.substring(1); - model = await modelLoader.loadExisting(id); + id = location.hash.slice(1); + const loadResponse = await modelLoader.loadExisting(id); + model = loadResponse.model; + migrationTool = loadResponse.migrationTool; } // The Migrator takes the starting state (model and id) and watches for a migration proposal. It encapsulates // the migration logic and just lets us know when a new model is loaded and available (with the "migrated" event). // It also takes a dataTransformationCallback to help in transforming data export format to be compatible for // import with newly created models. + // TODO: Consider just passing the ModelLoader (or even the model loader construction args?) and kind of wrapping it. + // Then this becomes something like a MigratingModelLoader. Then the model can have a migrationTool but sort of hide it. const migrator = new Migrator( modelLoader, model, + migrationTool, id, inventoryListDataTransformationCallback, ); migrator.events.on("migrated", () => { - model.close(); + model.dispose(); model = migrator.currentModel; - render(model); + migrationTool = migrator.currentMigrationTool; + renderModel(model, migrationTool); updateTabForId(migrator.currentModelId); }); // If the ModelLoader doesn't know how to load the model required for migration, it emits "migrationNotSupported". @@ -131,8 +146,8 @@ async function start(): Promise { // } // In this demo however, we trigger the proposal through the debug buttons. - render(model); + renderModel(model, migrationTool); updateTabForId(id); } -start().catch((error) => console.error(error)); +await start(); diff --git a/examples/version-migration/schema-upgrade/src/view/appView.tsx b/examples/version-migration/separate-container/src/view/appView.tsx similarity index 56% rename from examples/version-migration/schema-upgrade/src/view/appView.tsx rename to examples/version-migration/separate-container/src/view/appView.tsx index 93e8725eab8d..451c0285827e 100644 --- a/examples/version-migration/schema-upgrade/src/view/appView.tsx +++ b/examples/version-migration/separate-container/src/view/appView.tsx @@ -3,7 +3,8 @@ * Licensed under the MIT License. */ -import type { IMigratableModel } from "@fluid-example/example-utils"; +// eslint-disable-next-line import/no-internal-modules +import type { IMigrationTool } from "@fluid-example/migration-tools/internal"; import React, { useEffect, useState } from "react"; import type { IInventoryListAppModel } from "../modelInterfaces.js"; @@ -11,9 +12,10 @@ import type { IInventoryListAppModel } from "../modelInterfaces.js"; import { InventoryListView } from "./inventoryView.js"; export interface IInventoryListAppViewProps { - // TODO: All we really want here is a "readonly" indicator - maybe don't need the full IMigratableModel interface. - // Would maybe be better to grab that info from the Migrator rather than the MigrationTool anyway. - model: IInventoryListAppModel & IMigratableModel; + model: IInventoryListAppModel; + // TODO: All we really want here is a "readonly" indicator - maybe don't need the full IMigrationTool interface. + // Would maybe be better to grab that info from the Migrator rather than the MigrationTool anyway? + migrationTool: IMigrationTool; } /** @@ -25,26 +27,26 @@ export interface IInventoryListAppViewProps { export const InventoryListAppView: React.FC = ( props: IInventoryListAppViewProps, ) => { - const { model } = props; + const { model, migrationTool } = props; const [disableInput, setDisableInput] = useState( - model.migrationTool.migrationState !== "collaborating", + migrationTool.migrationState !== "collaborating", ); useEffect(() => { - const migrationStateChangedHandler = () => { - setDisableInput(model.migrationTool.migrationState !== "collaborating"); + const migrationStateChangedHandler = (): void => { + setDisableInput(migrationTool.migrationState !== "collaborating"); }; - model.migrationTool.events.on("stopping", migrationStateChangedHandler); - model.migrationTool.events.on("migrating", migrationStateChangedHandler); - model.migrationTool.events.on("migrated", migrationStateChangedHandler); + migrationTool.events.on("stopping", migrationStateChangedHandler); + migrationTool.events.on("migrating", migrationStateChangedHandler); + migrationTool.events.on("migrated", migrationStateChangedHandler); migrationStateChangedHandler(); return () => { - model.migrationTool.events.off("stopping", migrationStateChangedHandler); - model.migrationTool.events.off("migrating", migrationStateChangedHandler); - model.migrationTool.events.off("migrated", migrationStateChangedHandler); + migrationTool.events.off("stopping", migrationStateChangedHandler); + migrationTool.events.off("migrating", migrationStateChangedHandler); + migrationTool.events.off("migrated", migrationStateChangedHandler); }; - }, [model]); + }, [migrationTool]); return ; }; diff --git a/examples/version-migration/schema-upgrade/src/view/debugView.tsx b/examples/version-migration/separate-container/src/view/debugView.tsx similarity index 54% rename from examples/version-migration/schema-upgrade/src/view/debugView.tsx rename to examples/version-migration/separate-container/src/view/debugView.tsx index d2857838ba81..0d0c3f48b393 100644 --- a/examples/version-migration/schema-upgrade/src/view/debugView.tsx +++ b/examples/version-migration/separate-container/src/view/debugView.tsx @@ -3,44 +3,54 @@ * Licensed under the MIT License. */ -import type { IMigratableModel, MigrationState } from "@fluid-example/example-utils"; +import type { + IMigratableModel, + IMigrationTool, + MigrationState, + // eslint-disable-next-line import/no-internal-modules +} from "@fluid-example/migration-tools/internal"; import React, { useEffect, useState } from "react"; import type { IInventoryListAppModel } from "../modelInterfaces.js"; export interface IDebugViewProps { model: IInventoryListAppModel & IMigratableModel; + migrationTool: IMigrationTool; getUrlForContainerId?: (containerId: string) => string; } export const DebugView: React.FC = (props: IDebugViewProps) => { - const { model, getUrlForContainerId } = props; + const { model, migrationTool, getUrlForContainerId } = props; const [disableControls, setDisableControls] = useState( - model.migrationTool.migrationState !== "collaborating", + migrationTool.migrationState !== "collaborating", ); useEffect(() => { - const migrationStateChangedHandler = () => { - setDisableControls(model.migrationTool.migrationState !== "collaborating"); + const migrationStateChangedHandler = (): void => { + setDisableControls(migrationTool.migrationState !== "collaborating"); }; - model.migrationTool.events.on("stopping", migrationStateChangedHandler); - model.migrationTool.events.on("migrating", migrationStateChangedHandler); - model.migrationTool.events.on("migrated", migrationStateChangedHandler); + migrationTool.events.on("stopping", migrationStateChangedHandler); + migrationTool.events.on("migrating", migrationStateChangedHandler); + migrationTool.events.on("migrated", migrationStateChangedHandler); migrationStateChangedHandler(); return () => { - model.migrationTool.events.off("stopping", migrationStateChangedHandler); - model.migrationTool.events.off("migrating", migrationStateChangedHandler); - model.migrationTool.events.off("migrated", migrationStateChangedHandler); + migrationTool.events.off("stopping", migrationStateChangedHandler); + migrationTool.events.off("migrating", migrationStateChangedHandler); + migrationTool.events.off("migrated", migrationStateChangedHandler); }; - }, [model]); + }, [migrationTool]); return (

Debug info

- + @@ -50,54 +60,52 @@ export const DebugView: React.FC = (props: IDebugViewProps) => interface IMigrationStatusViewProps { readonly model: IMigratableModel; + readonly migrationTool: IMigrationTool; readonly getUrlForContainerId?: (containerId: string) => string; } const MigrationStatusView: React.FC = ( props: IMigrationStatusViewProps, ) => { - const { model, getUrlForContainerId } = props; + const { model, migrationTool, getUrlForContainerId } = props; const [migrationState, setMigrationState] = useState( - model.migrationTool.migrationState, + migrationTool.migrationState, ); useEffect(() => { - const migrationStateChangedHandler = () => { - setMigrationState(model.migrationTool.migrationState); + const migrationStateChangedHandler = (): void => { + setMigrationState(migrationTool.migrationState); }; - model.migrationTool.events.on("stopping", migrationStateChangedHandler); - model.migrationTool.events.on("migrating", migrationStateChangedHandler); - model.migrationTool.events.on("migrated", migrationStateChangedHandler); + migrationTool.events.on("stopping", migrationStateChangedHandler); + migrationTool.events.on("migrating", migrationStateChangedHandler); + migrationTool.events.on("migrated", migrationStateChangedHandler); migrationStateChangedHandler(); return () => { - model.migrationTool.events.off("stopping", migrationStateChangedHandler); - model.migrationTool.events.off("migrating", migrationStateChangedHandler); - model.migrationTool.events.off("migrated", migrationStateChangedHandler); + migrationTool.events.off("stopping", migrationStateChangedHandler); + migrationTool.events.off("migrating", migrationStateChangedHandler); + migrationTool.events.off("migrated", migrationStateChangedHandler); }; - }, [model]); + }, [migrationTool]); const proposedVersionStatus = - model.migrationTool.proposedVersion === undefined + migrationTool.proposedVersion === undefined ? "No proposed version for migration yet" - : `Proposed version to migrate to: ${model.migrationTool.proposedVersion}`; + : `Proposed version to migrate to: ${migrationTool.proposedVersion}`; const acceptedVersionStatus = - model.migrationTool.acceptedMigration === undefined + migrationTool.acceptedMigration === undefined ? "No accepted version for migration yet" - : `Accepted version to migrate to: ${model.migrationTool.acceptedMigration.newVersion} @ sequenceNumber: ${model.migrationTool.acceptedMigration.migrationSequenceNumber}`; + : `Accepted version to migrate to: ${migrationTool.acceptedMigration.newVersion} @ sequenceNumber: ${migrationTool.acceptedMigration.migrationSequenceNumber}`; - const migratedContainerStatus = (() => { - if (model.migrationTool.newContainerId === undefined) { - return "No migrated container yet"; + const migratedContainerStatus = ((): JSX.Element => { + if (migrationTool.newContainerId === undefined) { + return <>No migrated container yet; } - const navToNewContainer = () => { - if ( - model.migrationTool.newContainerId !== undefined && - getUrlForContainerId !== undefined - ) { - location.href = getUrlForContainerId(model.migrationTool.newContainerId); + const navToNewContainer = (): void => { + if (migrationTool.newContainerId !== undefined && getUrlForContainerId !== undefined) { + location.href = getUrlForContainerId(migrationTool.newContainerId); location.reload(); } }; @@ -106,13 +114,13 @@ const MigrationStatusView: React.FC = ( // Otherwise just use the string representation of the container id. const migratedReference = getUrlForContainerId === undefined ? ( - model.migrationTool.newContainerId + migrationTool.newContainerId ) : ( - {model.migrationTool.newContainerId} + {migrationTool.newContainerId} ); @@ -145,7 +153,7 @@ interface IControlsViewProps { const ControlsView: React.FC = (props: IControlsViewProps) => { const { proposeVersion, addItem, disabled } = props; - const addSampleItems = () => { + const addSampleItems = (): void => { addItem("Alpha", 1); addItem("Beta", 2); addItem("Gamma", 3); @@ -158,7 +166,7 @@ const ControlsView: React.FC = (props: IControlsViewProps) = Propose version: