Repository navigation
Conversation
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 Hartman (jason-ha)
left a comment
There was a problem hiding this comment.
Changes are viable as is.
Notes on optional improvements - especially a path to not lose type-safety for IShareMap use.
| * SemVer string. `"2.0.0"` preserves collaboration with Fluid 2.0.0-era clients and matches | ||
| * Fluid 3's own default. |
There was a problem hiding this comment.
Fluid 3 has no default. Consumers must always pass a value. "2.0.0" though is the lowest supported version.
| "@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" |
There was a problem hiding this comment.
Unless these picked up /beta or /legacy imports (not something I see), then these can be "^2.102.0 || ^3.0.0".
There was a problem hiding this comment.
Still can use ^2 || ^3 here to simplify maintenance. Not a blocker.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
| * Always use `||` to separate range clauses, never a single `|` — a single pipe fails when | ||
| * consumers install with yarn. |
There was a problem hiding this comment.
I think you can drop this comment.
| * @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; |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
| ## 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. |
There was a problem hiding this comment.
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.
…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
2c87468 to
2b1dca2
Compare
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
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
| // These overloads precede FluidMap's void returns without changing its forEach callback type. | ||
| delete(key: string): boolean; | ||
| set(key: string, value: TData): TypedSharedMap<TData>; |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
| "@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" |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.

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.0across packages, samples, and test utilities. The lockfile resolves Fluid 3.0.1.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.LogLevel.default/.erroraliases with.info/.essential, preserving their numeric values.SharedMap typing
IUseSharedMapResults<TData>.sharedMapnow usesTypedSharedMap<TData> | undefined, preserving typedget,forEach,values, andentries, booleandelete, and chainablesetwith 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'sforEachcallback. Hook runtime behavior is unchanged.Consumer-facing type differences and caveats:
Map. After checking it is defined,new Map(sharedMap)creates a local, unsynchronized snapshot.forEachcallback argument has Fluid'svoidmutator returns. Use the outersharedMapfor boolean deletion results or chaining.setremains permissive; usesetEntryforTData-constrained writes. UsesharedMap.entries()for typed iteration; directfor...of sharedMapretains 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: