Skip to content

Support Fluid Framework 3, and bump to 2.0.0-internal.19 - #889

Open
huntj88 wants to merge 10 commits into
mainv2from
user/jameshunt/2.0.0-internal.19
Open

huntj88 wants to merge 10 commits into
mainv2from
user/jameshunt/2.0.0-internal.19

Conversation

@huntj88

@huntj88 huntj88 commented Sep 9, 2026 •

Copy link
Copy Markdown
Contributor

Stacked on #888, which contains the ESM-only build/test migration.

Summary

Adds Fluid Framework 3 support alongside Fluid 2 in 2.0.0-internal.19, with dependency range >=2.102 <2.120 || >=3.0.0 <3.10.0 across packages, samples, and test utilities. The lockfile resolves Fluid 3.0.1.

  • Replace the legacy container compatibility argument with FluidOldestSupportedClient = "2.0.0". Fluid 2.102.0 is the first version accepting the SemVer form required by Fluid 3. This raises the installation/build floor without raising the configured collaboration compatibility floor. The constant remains unannotated to support both Fluid type names.
  • Replace removed LogLevel.default / .error aliases with .info / .essential, preserving their numeric values.

SharedMap typing

IUseSharedMapResults<TData>.sharedMap now uses TypedSharedMap<TData> | undefined, preserving typed get, forEach, values, and entries, boolean delete, and chainable set with DDS members.

This uses a local MIT-licensed copy of Fluid's map interfaces, not re-exported from the package entry point. Mutator overrides precede FluidMap & SharedMap: changing the copied signatures directly breaks assignability through Fluid's forEach callback. Hook runtime behavior is unchanged.

Consumer-facing type differences and caveats:

  • On Fluid 3, the result is no longer assignable to a built-in Map. After checking it is defined, new Map(sharedMap) creates a local, unsynchronized snapshot.
  • The third forEach callback argument has Fluid's void mutator returns. Use the outer sharedMap for boolean deletion results or chaining.
  • The inherited generic set remains permissive; use setEntry for TData-constrained writes. Use sharedMap.entries() for typed iteration; direct for...of sharedMap retains its previous untyped value inference.

Validation

Local validation on Node 24.14.0, with separate installations and all relevant Fluid dependencies aligned to each tested version:

Check 2.102.0 (floor) 3.0.1 (locked)
All packages, test utilities, and samples build Pass Pass
Live Share / canvas / media tests 81 / 17 / 57 passing 81 / 17 / 57 passing
Targeted SharedMap assignment, inference, chaining, and iteration type checks Pass Pass

Fluid Framework 3.0 is ESM-only, so a CommonJS build of Live Share cannot
consume it under any tsc configuration. This removes the CJS build and moves
the remaining CommonJS/Node10 tooling to ESM/Node16, which is a prerequisite
for supporting Fluid 3. No Fluid version ranges change here; this is verified
against the currently locked Fluid 2.110.0.

Live Share v2 is still marked internal, so the breaking packaging change is
acceptable now.

Packaging (breaking for CommonJS consumers):
- Remove "main" and reduce each "exports" entry to its "import" condition, so
  require() fails cleanly with ERR_PACKAGE_PATH_NOT_EXPORTED rather than
  resolving ESM through "default" and half-working on Node >= 22.12.
- Delete the five tsconfig.cjs.json files and the --cjs build task.
- Delete internal/usage-test/cjs-test and its CI step.

Build and test tooling:
- Point the three tsconfig.test.json files at the ESM/Node16 base. They
  previously extended tsconfig.cjs.json, which meant the entire mocha suite
  compiled as CommonJS with Node10 resolution.
- Replace ts-mocha with mocha in the test scripts. The suites run compiled
  JavaScript, so registering ts-node only conflicted with ESM.
- Migrate internal/test-utils to Node16/ESM.
- Add the .js extensions Node16 ESM requires to relative imports.
- Report build failures with the failing package and tsc exit code instead of
  an unhandled promise rejection.

Verified on Fluid 2.110.0: all packages and samples build, live-share 81,
live-share-media 57 and live-share-canvas 17 tests pass, the ESM usage test
passes, and npm run doctor is clean.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 7807308b-8b2a-4390-813d-a986277cec42

@jason-ha Jason Hartman (jason-ha) left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changes are viable as is.
Notes on optional improvements - especially a path to not lose type-safety for IShareMap use.

Comment on lines +50 to +51
* SemVer string. `"2.0.0"` preserves collaboration with Fluid 2.0.0-era clients and matches
* Fluid 3's own default.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fluid 3 has no default. Consumers must always pass a value. "2.0.0" though is the lowest supported version.

Comment thread internal/test-utils/package.json Outdated
"@fluidframework/telemetry-utils": "^2.0.0",
"@fluidframework/test-utils": "^2.0.0"
"@fluidframework/telemetry-utils": ">=2.102 <2.120 || >=3.0.0 <3.10.0",
"@fluidframework/test-utils": ">=2.102 <2.120 || >=3.0.0 <3.10.0"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unless these picked up /beta or /legacy imports (not something I see), then these can be "^2.102.0 || ^3.0.0".

@jason-ha Jason Hartman (jason-ha) Sep 15, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Still can use ^2 || ^3 here to simplify maintenance. Not a blocker.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The latest commit uses  ^2.102.0 || ^3.0.0  for the internal test utilities, so future 2.x/3.x releases no longer need range updates. kept the 2.102 floor to stay aligned with the SDK’s minimum and avoid older-version incompatibilities. Published SDK ranges remain bounded.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was not suggesting use of ^ to add risk. I suggested assuming the specs were good before.
After a search and also noting use of @fluid-internal/test-driver-definitions I see that was not the case. test-utils is definitely doing the unstable stuff.
Since there are internal uses only exact is correct if this package went external. I agree with the new README statement.

Comment on lines +14 to +15
* Always use `||` to separate range clauses, never a single `|` — a single pipe fails when
* consumers install with yarn.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you can drop this comment.

Comment on lines +81 to +88
* @remarks
* Previously typed as `Map<string, TData> & SharedMap`. Fluid 3.0 changed `ISharedMap` to
* extend Fluid's own `FluidMap` rather than the built-in `Map`, so that intersection is no
* longer satisfiable — and `FluidMap` cannot be named here because it does not exist in
* Fluid 2.x, which we still support. Use the typed `getEntry` / `setEntry` callbacks below
* for value-typed access.
*/
sharedMap: (Map<string, TData> & SharedMap) | undefined;
sharedMap: SharedMap | undefined;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a little unfortunate on the FF side.
We do want to distance APIs from built-in types since they can change shape and FF might not implement the new features.
The incompatibility here was not intentional and we might patch that for 3.1 that is too late. The alternative here is to make a local copy of https://github.com/microsoft/FluidFramework/blob/main/packages/common/core-interfaces/src/fluidMap.ts and type as FluidMap<string, TData> &. That should be viable for all 2.x and 3.x versions of FF.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looking at the local-copy approach, with one addition. I have not pushed this code yet, just wanted your opinion first

FluidMap<string, TData> & SharedMap restores get, but FluidMap's delete(key): void / set(key, value): void shadow ISharedMap's own boolean / this, since the FluidMap half comes first in overload order. That would cost consumers if (map.delete(k)) and .set(a, b).set(c, d) — breaks the shipped typing never had. An overrides interface, first in the intersection, puts them back:

export interface ISharedMapValueOverrides<TData> {
    delete(key: string): boolean;
    set(key: string, value: TData): TypedSharedMap<TData>;
}

export type TypedSharedMap<TData> = ISharedMapValueOverrides<TData> &
    FluidMap<string, TData> &
    SharedMap;

These only restate what ISharedMap already declares, so they match runtime — and FluidMap's doc comment anticipates it ("subtypes may override this to return a boolean if appropriate").

Verified with tsc --strict on both ends of the range, 2.102.0 and 3.0.1, assigning a real SharedMap in a generic context as useSharedMap does: get, delete, set chaining, forEach / values / entries / for..of, and the DDS members all behave as they do today. The one thing no typing can preserve is const m: Map<string, TData> = sharedMap — Fluid 3's SharedMap isn't a Map, so that's TS2322 now; consumers copy (new Map(sharedMap)) or use getEntry / setEntry. Release-notes item.

The copy lives in packages/live-share-react/src/types/FluidMap.ts, verbatim apart from our prettier config, and isn't re-exported from the entry point so the names can't collide with Fluid's own. It gets deleted when we drop 2.x.

Before I push — do the delete / set overrides look safe to you long-term, and if the 3.1 patch lands, does the local copy become unnecessary?

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The return type of delete and set should be boolean and this respectively for 3.0+. At least that is what I get with this mock up:

interface ISharedMap
	extends FluidMap<string, any> {
	get<T = any>(key: string): T | undefined;
	set<T = unknown>(key: string, value: T): this;
	delete(key: string): boolean;
}
type setReturn = ReturnType<(FluidMap<string, any> & ISharedMap)["set"]>;
type deleteReturn = ReturnType<(FluidMap<string, any> & ISharedMap)["delete"]>;

For <3.0, I see the same.
It would be okay for live-share FluidMap to declare set and delete to match ISharedMap declarations since that is the only place things are combined.

3.1 won't really solve the issue.

Comment thread FLUID-V3-MIGRATION.md Outdated
Comment on lines +264 to +282
## 5. The CJS build cannot support Fluid 3

This is a hard constraint, not a matter of effort.

- All five `packages/*/tsconfig.cjs.json` use `"module": "CommonJS"` + `"moduleResolution": "Node10"`.
Fluid 3 removed its Node10 type-declaration entrypoints, so every Fluid import fails TS2307.
- It cannot be fixed by switching resolution: `module: CommonJS` with `moduleResolution: Bundler`
is rejected outright with **TS5095** ("bundler can only be used when module is set to
preserve or to es2015 or later").
- It cannot be fixed by switching to Node16 either — a CJS-context consumer importing an
ESM-only package fails TS1479 by design.

So `bin/cjs` cannot be produced against Fluid 3 with any tsc configuration. A CJS artifact
could in principle still be _emitted_ via a separate transpiler, but its `.d.ts` would still
reference ESM-only Fluid types and would fail for the consumer — so this does not rescue it.

**Conclusion: CJS consumers of Live Share cannot use Fluid 3.** This is imposed by Fluid, not
by us. Note also that `exports` maps cannot express a per-format peer-dependency range, so a
"CJS means Fluid 2" split is a documented support-matrix statement that npm cannot enforce.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Node20+ and TS5.9+ support require-esm. This would be sufficient to retain CommonJS and use FF 3.x, but moving to just ESM is probably better. The same require-esm feature will allow live-share CommonJS consumers to use ESM only version of live-share packages.
Official FF statement is that CommonJS is not supported as FF wants to reserve the right to use top-level awaits. FF would only actually do so at a 3.x0.0 boundary; so, the current >=3.0 <3.10 dep spec protects against trouble.

This comment is really just for give complete information.

Comment thread FLUID-V3-MIGRATION.md Outdated
James Hunt and others added 2 commits September 14, 2026 16:24
…oints

Follow-ups to review comments on #888.

Build:
- Raise "target" and "lib" from es6/es2020 to ES2022 across the five packages
  and internal/test-utils. This matches Fluid Framework 3.x, which builds at
  ES2022 in common/build/build-common/tsconfig.base.json.
- Leave "useDefineForClassFields" unset so it takes its ES2022 default of true,
  again matching Fluid. Live Share now compiles with standard ECMAScript class
  field semantics rather than TypeScript's legacy assignment semantics. Fluid
  only opts out in the experimental PropertyDDS packages, which depend on the
  legacy behavior; nothing here does.
- Drop "moduleResolution", which is implied by "module": "Node16".

Packaging:
- Remove the top-level "module" and "types" fields. Resolution already goes
  through "exports", and CommonJS/Node10 consumers were dropped in the previous
  commit.
- Reduce each "exports" entry to "import": "<path>.js". The "types" condition
  is only required when the .d.ts is not adjacent to the .js, and every package
  emits both into the same directory.

Verified: all packages and samples build, live-share 81, live-share-media 57
and live-share-canvas 17 tests pass, and the ESM and pnpm usage tests pass.
Type-checking every project with useDefineForClassFields enabled reports no
class field collisions.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 59286b4a-1977-4cb4-a0f8-e50fde36924e
Adds Fluid Framework 3 support alongside Fluid 2. Builds on the ESM/Node16
migration in the previous commit, which is a hard prerequisite: Fluid 3 is
ESM-only, so a CommonJS build cannot consume it.

Version range becomes ">=2.102 <2.120 || >=3.0.0 <3.10.0".

Why the peer floor moves from 2.40 to 2.102:
Fluid 3.0 removed the legacy CompatibilityMode values ("1" / "2"), so the
container compatibility argument must be a SemVer string. That form is only
understood from 2.102.0 onward. Below it, "2.0.0" misses the lookup tables
that older releases use and the container is created with no compatibility
options at all -- silently, with no error. 2.102 is also exactly where
LogLevel.info / LogLevel.essential appear, so both breaks resolve at the same
floor.

This does not drop support for Fluid 2.40 clients. The peer range governs what
a consumer installs to build against; oldestSupportedClient governs which
clients can still collaborate on our documents, and it is set to "2.0.0" --
lower than 2.40, so 2.40-era clients keep working.

API changes:
- FluidCompatibilityMode -> FluidOldestSupportedClient, value "2.0.0". Left
  unannotated deliberately: the Fluid type is MinimumVersionForCollab in
  2.102-2.115 and OldestSupportedClientVersion from 2.116, so naming either
  would break against part of the supported range. The inferred literal type
  satisfies both.
- LogLevel.default / LogLevel.error were removed in 3.0. Replaced with
  LogLevel.info / LogLevel.essential, which carry identical values (20 / 30)
  and exist in both 2.102+ and 3.x.
- IUseSharedMapResults.sharedMap narrows from (Map<string, TData> & SharedMap)
  to SharedMap. Fluid 3 changed ISharedMap to extend its own FluidMap rather
  than the built-in Map, and FluidMap cannot be named here because it does not
  exist in Fluid 2.x. Note for release notes: this loosens types rather than
  breaking builds, so sharedMap.get(key) still compiles but now yields any.
  Consumers should pass the type argument or use getEntry.
- internal/test-utils Fluid pins moved from ^2.0.0 into the managed range.

Verified on Fluid 3.0.1: all packages and samples build, live-share 81,
live-share-media 57 and live-share-canvas 17 tests pass, the ESM usage test
passes, and npm run doctor is clean.

FLUID-V3-MIGRATION.md records the full investigation and the decisions.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 7807308b-8b2a-4390-813d-a986277cec42
@huntj88
huntj88 force-pushed the user/jameshunt/2.0.0-internal.19 branch from 2c87468 to 2b1dca2 Compare September 14, 2026 23:16
James Hunt and others added 3 commits September 15, 2026 10:00
Documentation and comment corrections from review comments on #889. No
behavior changes.

- FluidOldestSupportedClient: Fluid 3 has no default minimum-version value, so
  a value must always be supplied. "2.0.0" is unchanged and remains correct as
  the lowest version Fluid 3 supports; only the justification was wrong.
- update-fluid-range.js: drop the note about separating range clauses with
  "||" rather than a single "|".
- FLUID-V3-MIGRATION.md: require(esm) was backported to Node 20.19, so the
  runtime support line is ">= 20.19" rather than ">= 22.12 / fails on 18 / 20".
- FLUID-V3-MIGRATION.md: scope the CJS conclusion to the build. We cannot ship
  a tsc-built CJS artifact against Fluid 3, but CJS consumers are not blocked
  outright, since Node >= 20.19 and TypeScript 5.9+ support require(esm).
- FLUID-V3-MIGRATION.md: state the consequence of declaring no "engines" field,
  rather than only noting its absence.

Verified against both ends of the supported range. At Fluid 2.102.0 and 3.0.1
all packages and samples build and live-share 81, live-share-media 57 and
live-share-canvas 17 tests pass. The ESM and pnpm usage tests pass on 3.0.2.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 59286b4a-1977-4cb4-a0f8-e50fde36924e
Vendor Fluid map interfaces and preserve typed reads, boolean deletion, and chainable writes with TypedSharedMap overrides. Document the remaining native Map and callback typing limitations.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 9e828dbb-c789-4b25-96f0-7e0b7dd0242a
Remove the standalone investigation document; keep the migration summary, compatibility caveats, and validation results in PR #889.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 9e828dbb-c789-4b25-96f0-7e0b7dd0242a
@huntj88
huntj88 marked this pull request as ready for review September 15, 2026 22:03
Base automatically changed from user/jameshunt/drop-cjs-esm-build to mainv2 September 15, 2026 22:19
James Hunt and others added 2 commits September 15, 2026 16:21
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 5e52687a-12e0-4cd8-977a-80616ce76113
Allow Fluid ^2.102.0 || ^3.0.0 in the developer-only test harness while retaining bounded ranges for published SDK packages. Update lockfile metadata and document dependency alignment for compatibility checks.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 6d962410-cbd7-4339-b699-e2767a57c1c5
Comment on lines +77 to +79
// These overloads precede FluidMap's void returns without changing its forEach callback type.
delete(key: string): boolean;
set(key: string, value: TData): TypedSharedMap<TData>;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am a little surprised these are needed. My quick tests said they weren't needed. Not a blocker but I am curious what I missed.

@huntj88 huntj88 Sep 21, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The difference is  ReturnType<>  versus actual calls: your probe picks the last signature, while calls here match  FluidMap ’s earlier void overloads. Without the overrides, boolean deletion checks and set chaining fail. Confirmed with TS 5.5.4 / Fluid 3.0.1.

Changing the copied FluidMap signatures instead breaks assignability through forEach’s callback-map type, so the separate overrides preserve both compatibility and the expected return types.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe something like this instead?

image

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given the elements involved I don't see a better solution.
I've learned that the competing definitions are not combined but are treated as overloads. Type processing picks the last signature, but an actual call uses the first that matches and they all match. Tricky.

Comment thread internal/test-utils/package.json Outdated
"@fluidframework/telemetry-utils": "^2.0.0",
"@fluidframework/test-utils": "^2.0.0"
"@fluidframework/telemetry-utils": ">=2.102 <2.120 || >=3.0.0 <3.10.0",
"@fluidframework/test-utils": ">=2.102 <2.120 || >=3.0.0 <3.10.0"

@jason-ha Jason Hartman (jason-ha) Sep 15, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Still can use ^2 || ^3 here to simplify maintenance. Not a blocker.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: ef6b77af-295d-428b-9806-d51ee8baabb2
Omit FluidMap's set and delete signatures so SharedMap supplies its native mutator return types while preserving forEach callback compatibility.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 403384ca-b379-48c8-8b11-0e73224b7302

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When I looked at live-share code to see exactly what you were seeing, I noticed that at least @microsoft/08.3d-model is not well configured. Since live-share-react is import only and @microsoft/08.3d-model is CommonJS (no "type": "module" in package.json and tsconfig.json is using "module": "Node16"), these are unhappy. I had a completely successful "build"; so, I guess something is messing with settings or doesn't respect the full rules.
04.live-share-react tsconfig.json is using ESNext+Bundler.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants