@collagejs/core 0.5.0 β†’ 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  > Micro library for framework-agnostic micro-frontends
4
4
 
5
- **🚧🚧 I'M LATE. WORK IN PROGRESS... ETA: JULY, 2026 🚧🚧**
5
+ **🚧 I'M CATCHING UP FAST. WORK IN PROGRESS... ETA: JULY, 2026 🚧**
6
6
 
7
7
  **If you're interested, star ⭐ the repository to get updates on the progress in your GH homepage.**
8
8
 
@@ -26,6 +26,7 @@ type UnmountFn = () => Promise<void>;
26
26
  interface CorePiece<TProps, TCap> {
27
27
  mount: (target: HTMLElement | ShadowRoot, props?: TProps) => Promise<UnmountFn>;
28
28
  update?: (props: TProps) => Promise<void>;
29
+ relocate?: (source: AcceptableTarget, target: AcceptableTarget) => Promise<'supported' | 'done' | 'unsupported'>
29
30
  readonly capabilities?: CorePieceCapabilities & TCap;
30
31
  };
31
32
  ```
@@ -35,9 +36,10 @@ In short: Micro-frontend creators must simply provide a way to generate an obje
35
36
 
36
37
  - `mount` mounts the micro-frontend user interface in the document
37
38
  - `update` updates the properties given to the micro-frontend
39
+ - `relocate` migrates all root DOM elements in the current target to the new target without a mounting cycle
38
40
  - `capabilities` is a feature introduced in v0.5.0 that allows core piece objects to declare its capabilities (see more in its own section)
39
41
 
40
- The `update` function is optional. The `mount` function must return a cleanup function that, ideally, reverts the mounting process.
42
+ Only the `mount` function is required and must return a cleanup function that, ideally, reverts the mounting process.
41
43
 
42
44
  This object, once obtained by the consuming project/micro-frontend, is given to the `<Piece>` component. The implementation of this component is framework-specific. For example, the `<Piece>` component found in the `@collagejs/svelte` package is a **Svelte** component.
43
45
 
@@ -78,7 +80,8 @@ export function buildTestPiece<TProps extends Record<string, any> = Record<strin
78
80
  callbacks?.update?.(props);
79
81
  pre.textContent = JSON.stringify(props, null, 2);
80
82
  return Promise.resolve();
81
- }
83
+ },
84
+ relocate: () => Promise.resolve('supported'),
82
85
  };
83
86
  }
84
87
  ```
@@ -90,11 +93,10 @@ The key learnings here are:
90
93
  2. Inside `mount()`, we do whatever we need to do to mount our user interface inside the target element.
91
94
  3. We return a cleanup function that unmounts the user interface.
92
95
  4. We optionally provide the `update()` method for property reactivity.
96
+ 5. We optionally provide the `relocate()` method for runtime flexibility. Fun fact: *CollageJS* can mount in shadow DOM, and reactively changing this setting triggers relocation.
93
97
 
94
98
  You can export from a single project as many of these factory functions as desired. You are not constrained to expose just one *CollageJS* piece per project. Export as many as needed/wanted.
95
99
 
96
- This is how packages like `@collagejs/svelte` work: When mounting, it calls Svelte's `mount()` function with the given options (if any are given), and returns a cleanup function that calls `unmount()` on the component. Updating properties is as simple as using a reactive object, at which point Svelte itself takes over the reacting part.
97
-
98
100
  ## Router Needs
99
101
 
100
102
  As you may (or may not) know, `single-spa` is a client-side router that defines a contract similar to `CorePiece` above. *CollageJS*, on the other hand, doesn't provide a router. Use whichever client-side router you wish in your micro-frontend projects.
@@ -108,7 +110,7 @@ There are 2 reasons:
108
110
 
109
111
  Yes, it is very handy to mount/dismount MFE's as the location URL changes. This makes routing a popular choice, but not a mandatory one. MFE's can come and go for any reason, like button clicks, timers or anything a developer can imagine.
110
112
 
111
- Maintenance is an issue. The `single-spa` core team has had hard times trying to accommodate everyone's needs, especially when dealing with clashing behavior between their router and some framework's router, because people create MFE's with their own router built-in. On top of this, `single-spa`'s layout web component, which is a "define your routes in markup" helper, lacks features that people want because they exist in other router engines.
113
+ Maintenance is an issue. The `single-spa` core team has had hard times trying to accommodate everyone's needs, especially when dealing with clashing behavior between their router and some framework's router, because people create MFE's with their own router built-in. On top of this, `single-spa`'s layout web component, which is a "define your routes in markup" helper (web component), lacks features that people want because they exist in other router engines.
112
114
 
113
115
  So, the conclusion here is: *CollageJS* takes care of micro-frontends. Just that.
114
116
 
@@ -132,19 +134,19 @@ You can easily create a new *CollageJS* root project by telling GitHub to create
132
134
  - How routers and routes are laid out in markup
133
135
  - How fallback content works
134
136
 
135
- > πŸͺ€**The Catch**: It is a Svelte project. Ideally, you should know [Svelte](https://svelte.dev) to take full advantage of it. With `single-spa`, you would have to learn how it worked, and how its layout web component worked. With this one, you should learn at least a little Svelte. Some learning curve on both sides. The good thing about this one is that Svelte knowledge is much more far-reaching. Svelte is very easy and fun. We promise.
137
+ > πŸͺ€**The Catch**: It is a Svelte project. Ideally, you should know [Svelte](https://svelte.dev) to take full advantage of it. With `single-spa`, you would have to learn how it worked, and how its layout web component worked. With this one, you should learn at least a little Svelte. Some learning curve exists on both sides. The good thing about this one is that Svelte knowledge is much more far-reaching. Svelte is very easy and fun. We promise.
136
138
 
137
139
  ## For the `single-spa` Savvy
138
140
 
139
- If you know/are used to `single-spa`, you're almost up to speed with *CollageJS*. Basically, there's no concept of a "root config" project, but of course, there's always a "root" project. Use whatever framework/technology you want to produce it.
141
+ If you know/are used to `single-spa`, you're almost up to speed with *CollageJS*. Basically, there's no concept of a "root config" project, but of course, there's always a "root" project. Use whichever framework/technology you want to produce it.
140
142
 
141
- Then the micro-frontends: The concept doesn't exist. At this point (after creating a root UI project), you're 100% free to do as you wish. Just mount *CollageJS pieces*, which are basically the equivalent of `single-spa` parcels. You don't get a router provided, you bring your own, or don't bring a router. No worries. Who says that a router is required? Not us. You can trigger "parcel" loading by any means at your disposal: Button clicks, timers, window events, and yes, also location URL changes (routing) if you want.
143
+ Then the micro-frontends: The concept doesn't exist. At this point (after creating a root UI project), you just mount *CollageJS pieces* which are basically the equivalent of `single-spa` parcels. You don't get a router provided, you bring your own, or don't bring a router. No worries. Who says that a router is required? Not us. You can trigger "parcel" loading by any means at your disposal: Button clicks, timers, window events, and yes, also location URL changes (routing) if you want.
142
144
 
143
145
  ### Technical Differences
144
146
 
145
147
  While `single-spa` asks you to shape your module exports in a particular way (the lifecycle functions), *CollageJS* imposes no such restriction. It is just not necessary. Just make sure you can get an object of type `CorePiece` to the `<Piece>` component of your preferred framework. Then use your framework's marvels to make the `<Piece>` component appear or disappear.
146
148
 
147
- Yes, you'll still be working with import maps. They are super handy. We provide an enhanced (and simplified at the same time) version of `import-map-overrides` named `@collagejs/imo`. It only supports the `overridable-importmap` type (and therefore only native import maps for native ES modules), but carries support for our `@collagejs/vite-aim` plug-in that lets you statically import from micro-frontends. **That's right! We are free from dynamic `import()` calls!** We can statically import from micro-frontends. Furthermore, it has a more modern user interface:
149
+ Yes, you'll still be working with import maps. They are super handy. We provide an enhanced (and simplified at the same time) version of `import-map-overrides` named `@collagejs/imo`. It only supports the `overridable-importmap` type (and therefore only native import maps for native ES modules), but carries support for our `@collagejs/vite-aim` plug-in that lets you statically import from micro-frontends and auto-externalizes anything in the import map. **That's right! We are free from dynamic `import()` calls!** We can statically import from micro-frontends. Furthermore, it has a more modern user interface:
148
150
 
149
151
  ![Main screen of @collagejs/imo](./_docs/collagejs-imo.png)
150
152
 
@@ -160,22 +162,18 @@ Gone. There's no equivalent in *CollageJS*, as experience with `single-spa` has
160
162
 
161
163
  ## I'm Curious about `capabilities`
162
164
 
163
- Ok, in a nutshell, what inspired this feature was implementing the ability to mount micro-frontends (and its CSS) inside shadow root objects, plus give the developers a good DX. In short: If you, the dev, said you wanted the MFE in an open shadow root, but then changed your mind and now you want a closed shadow root, we have to unmount and remount or some other hacky things, like a full page reload. But for this case, and at least in Svelte, we could relocate the component's generated DOM tree without remounting.
164
-
165
- This sounds great, but what if that breaks the MFE? So the safe path is not to take advantage of cool features. This conclusion made me sad as a developer. Therefore, enter `capabilties`.
166
-
167
- The `capabilities` object simply states what the core piece object is able to withstand. The core library defines 2 capabilities, and devs can add user-defined ones to the object.
165
+ The `capabilities` property is a place where core piece developers can declare what their pieces are capable of. The *CollageJS* core library defines a small set of these, but developers are free to define their own. These are useful for custom mounting implementations in large projects, where additional impositions can be placed on developed pieces. Because core pieces are closed in nature, this is a simple way to inform consumers about the piece's capabilities.
168
166
 
169
- One of the stock (or "official" if you will) capabilities is `relocatable: boolean`. If a core piece object returns `true` for `relocatable`, it is making this statement: "I can take DOM relocation operations without going through the mounting lifecycle".
167
+ > πŸ‘΄ If you're a C/C++ developer that knows COM/ActiveX, this is kind of the equivalent of `IUnknown::QueryInterface()`, where interfaces are layers of additional capabilities.
170
168
 
171
- With this reassurance at hand, framework adapters like `@collagejs/svelte` and others that can pull the trick cleanly can go ahead and do it, enhancing DX. This made me happy once more as a developer, and hope that it makes more devs happy.
169
+ Yes, you may also extend the core piece object and work with interfaces in TypeScript. Your choice.
172
170
 
173
171
  ## Packages
174
172
 
175
173
  | Package | Status | Links | Description |
176
174
  | - | - | - | - |
177
175
  | `@collagejs/core` | βœ”οΈ | (This repo) | Core functionality. Provides the general mounting and unmounting logic. |
178
- | `@collagejs/vite-css` | βœ”οΈ | [Repo](https://github.com/collagejs/vite) | Vite plug-in that offers a CSS-mounting algorithm that is fully compatible with Vite's CSS bundling, including split CSS. It also features FOUC prevention. |
176
+ | `@collagejs/vite-css` | βœ”οΈ | [Repo](https://github.com/collagejs/vite) | Vite plug-in that offers a CSS-mounting algorithm that is fully compatible with Vite's CSS bundling, including split CSS. It also features FOUC prevention, shadow root mounting and relocation. |
179
177
  | `@collagejs/vite-im` | βœ”οΈ | [Repo](https://github.com/collagejs/vite) | Vite plug-in that injects an import map and optionally the `@collagejs/imo` package to define bare module identifiers for easy micro-frontend loading and debugging. |
180
178
  | `@collagejs/vite-aim` | βœ”οΈ | [Repo](https://github.com/collagejs/vite) | Vite-plugin that auto-externalizes the module identifiers found in the application's import map. It receives the import map live (and with overrides) from the client. This enables static imports (no more dynamic `import()` calls). |
181
179
  | `@collagejs/imo` | βœ”οΈ | [Repo](https://github.com/collagejs/imo) | Our version of `import-map-overrides` that does the usual overriding of import map entries, plus it transmits the final import map to Vite development servers found in it. |
@@ -190,4 +188,4 @@ With this reassurance at hand, framework adapters like `@collagejs/svelte` and o
190
188
  | Repository | Description |
191
189
  | - | - |
192
190
  | [Root Template](https://github.com/collagejs/root-template) | Root template repository that can be used to create new repositories for *CollageJS* root projects, with client-side routing already configured. |
193
- | (-) | Repository of *CollageJS* pieces made in various front-end technologies for your reference and inspiration. |
191
+ | [Gallery](https://github.com/collagejs/gallery) | Repository of *CollageJS* pieces made in various front-end technologies for your reference and inspiration. |
@@ -7,5 +7,6 @@ export declare class MountedPiece<TProps extends Record<string, any> = Record<st
7
7
  [mountKey](target: AcceptableTarget, props?: TProps): Promise<void>;
8
8
  unmount(): Promise<void>;
9
9
  update(props: TProps): Promise<void>;
10
+ relocate(target: AcceptableTarget, newTarget: AcceptableTarget, customRelocate?: (source: AcceptableTarget, target: AcceptableTarget) => Promise<boolean>): Promise<boolean>;
10
11
  get capabilities(): (CorePieceCapabilities & TCap) | undefined;
11
12
  }
@@ -30,6 +30,66 @@ async function doUpdate(update, props) {
30
30
  }
31
31
  return await update(props);
32
32
  }
33
+ function relocationResultValue(result) {
34
+ if (Array.isArray(result)) {
35
+ return result[0];
36
+ }
37
+ return result;
38
+ }
39
+ async function doRelocate(relocate, target, newTarget) {
40
+ const rollbackFns = new Stack();
41
+ let safeState = true;
42
+ const doRelocateInternal = async (rel) => {
43
+ const maybePushRollback = (result) => {
44
+ if (Array.isArray(result) && (result[0] === 'done' || result[0] === 'supported')) {
45
+ rollbackFns.push(result[1]);
46
+ }
47
+ else if (result === 'done') {
48
+ safeState = false;
49
+ }
50
+ };
51
+ if (Array.isArray(rel)) {
52
+ let supported = false;
53
+ for (const fn of rel) {
54
+ const r = await doRelocateInternal(fn);
55
+ const rValue = relocationResultValue(r);
56
+ if (rValue === 'supported') {
57
+ supported = true;
58
+ }
59
+ else if (rValue === 'unsupported') {
60
+ if (safeState) {
61
+ while (rollbackFns.size) {
62
+ await rollbackFns.pop()?.();
63
+ }
64
+ return 'unsupported';
65
+ }
66
+ throw new Error("Relocation function returned 'unsupported' after another relocation function returned 'done' without a rollback. The piece's state is now inconsistent.");
67
+ }
68
+ }
69
+ return supported ? 'supported' : 'done';
70
+ }
71
+ try {
72
+ const r = await rel(target, newTarget);
73
+ maybePushRollback(r);
74
+ return relocationResultValue(r);
75
+ }
76
+ catch (error) {
77
+ if (safeState) {
78
+ while (rollbackFns.size) {
79
+ await rollbackFns.pop()?.();
80
+ }
81
+ console.warn("Relocation function failed. Piece relocation has been rolled back.", error);
82
+ return 'unsupported';
83
+ }
84
+ throw new Error("Relocation function failed after another relocation function returned 'done' without a rollback. The piece's state is now inconsistent.");
85
+ }
86
+ };
87
+ const internalResult = await doRelocateInternal(relocate);
88
+ if (rollbackFns.size > 0 && relocationResultValue(internalResult) === 'supported' && safeState) {
89
+ return rollbackFns;
90
+ }
91
+ return internalResult;
92
+ }
33
93
  export class MountedPiece {
34
94
  #piece;
35
95
  #id;
@@ -68,6 +128,35 @@ export class MountedPiece {
68
128
  update(props) {
69
129
  return doUpdate(this.#piece.update, props);
70
130
  }
131
+ async relocate(target, newTarget, customRelocate) {
132
+ if (!this.#piece.relocate) {
133
+ return Promise.resolve(false);
134
+ }
135
+ const result = await doRelocate(this.#piece.relocate, target, newTarget);
136
+ if (result === 'done') {
137
+ return true;
138
+ }
139
+ if (result === 'unsupported') {
140
+ return false;
141
+ }
142
+ // At this point, custom relocation must finish the job.
143
+ if (!customRelocate) {
144
+ throw new Error("Relocation of this piece is 'supported', but no custom relocation function was provided.");
145
+ }
146
+ if (result === 'supported') {
147
+ return await customRelocate(target, newTarget);
148
+ }
149
+ try {
150
+ return await customRelocate(target, newTarget);
151
+ }
152
+ catch (error) {
153
+ while (result.size) {
154
+ await result.pop()?.();
155
+ }
156
+ console.warn("Custom relocation function failed. Piece relocation has been rolled back.", error);
157
+ return false;
158
+ }
159
+ }
71
160
  get capabilities() {
72
161
  return this.#piece.capabilities;
73
162
  }
package/dist/index.d.ts CHANGED
@@ -3,3 +3,5 @@ export { mountPiece } from './mountPiece.js';
3
3
  export { mountPieceKey } from './common.js';
4
4
  export { ensureGlobalCollageJs } from './global.js';
5
5
  export { preventRemount } from './preventRemount.js';
6
+ export { noopPiece } from './noopPiece.js';
7
+ export { Stack } from './Stack.js';
package/dist/index.js CHANGED
@@ -2,3 +2,5 @@ export { mountPiece } from './mountPiece.js';
2
2
  export { mountPieceKey } from './common.js';
3
3
  export { ensureGlobalCollageJs } from './global.js';
4
4
  export { preventRemount } from './preventRemount.js';
5
+ export { noopPiece } from './noopPiece.js';
6
+ export { Stack } from './Stack.js';
@@ -0,0 +1,12 @@
1
+ import type { CorePiece } from "./types.js";
2
+ /**
3
+ * Utility function that provides a no-op `CorePiece` object. This is useful for testing or when a placeholder piece is
4
+ * needed.
5
+ *
6
+ * **⚠️IMPORTANT**: The returned object is a singleton. This is **not** a factory function that creates a new object
7
+ * each time it is called. If you need a new object, you should create your own.
8
+ * @template TProps The type of the properties for the `CorePiece`. Defaults to `Record<string, any>`.
9
+ * @template TCap The type of the capabilities for the `CorePiece`. Defaults to an empty object `{}`.
10
+ * @returns {CorePiece<TProps, TCap>} A no-op `CorePiece` object properly typed.
11
+ */
12
+ export declare function noopPiece<TProps extends Record<string, any> = Record<string, any>, TCap extends Record<string, any> = {}>(): CorePiece<TProps, TCap>;
@@ -0,0 +1,16 @@
1
+ const np = {
2
+ mount: () => Promise.resolve(() => Promise.resolve()),
3
+ };
4
+ /**
5
+ * Utility function that provides a no-op `CorePiece` object. This is useful for testing or when a placeholder piece is
6
+ * needed.
7
+ *
8
+ * **⚠️IMPORTANT**: The returned object is a singleton. This is **not** a factory function that creates a new object
9
+ * each time it is called. If you need a new object, you should create your own.
10
+ * @template TProps The type of the properties for the `CorePiece`. Defaults to `Record<string, any>`.
11
+ * @template TCap The type of the capabilities for the `CorePiece`. Defaults to an empty object `{}`.
12
+ * @returns {CorePiece<TProps, TCap>} A no-op `CorePiece` object properly typed.
13
+ */
14
+ export function noopPiece() {
15
+ return np;
16
+ }
package/dist/types.d.ts CHANGED
@@ -20,6 +20,29 @@ export type UnmountFn = () => Promise<void>;
20
20
  * @returns A promise to the cleanup function that unmounts the piece.
21
21
  */
22
22
  export type MountFn<TProps extends Record<string, any> = Record<string, any>> = (target: AcceptableTarget, props?: MountProps<TProps>) => Promise<UnmountFn>;
23
+ /**
24
+ * Supported return values of `CorePiece.relocate` functions.
25
+ */
26
+ export type RelocationResultValue = 'supported' | 'unsupported' | 'done';
27
+ /**
28
+ * Defines the signature of rollback functions that can be returned by `CorePiece.relocate` functions. These functions
29
+ * can be provided when returning `done` or `supported` from a relocation function, and will be called if the
30
+ * relocation chain fails.
31
+ * @returns A promise that resolves once the rollback process concludes.
32
+ */
33
+ export type RelocationRollbackFn = () => Promise<void>;
34
+ /**
35
+ * Defines the possible return values of `CorePiece.relocate`.
36
+ */
37
+ export type RelocationResult = RelocationResultValue | [Exclude<RelocationResultValue, 'unsupported'>, RelocationRollbackFn];
38
+ /**
39
+ * Type that defines the signature of the functions accepted in `CorePiece.relocate`.
40
+ * @param parent The current parent of the piece's root element(s).
41
+ * @param newParent The new parent where the piece's root element(s) will be relocated.
42
+ * @returns A promise that resolves to one of the acceptable values. See `RelocationResult` and related types for
43
+ * details.
44
+ */
45
+ export type RelocateFn = (parent: AcceptableTarget, newParent: AcceptableTarget) => Promise<RelocationResult>;
23
46
  /**
24
47
  * Type that defines the signature of the functions accepted in `CorePiece.update`.
25
48
  * @param props The new property values for the mounted piece.
@@ -33,7 +56,11 @@ export type Mount<TProps extends Record<string, any> = Record<string, any>> = Mo
33
56
  /**
34
57
  * Defines the accepted shapes for `CorePiece.update`.
35
58
  */
36
- export type Update<TProps extends Record<string, any> = Record<string, any>> = UpdateFn<TProps> | UpdateFn<TProps>[] | Update[];
59
+ export type Update<TProps extends Record<string, any> = Record<string, any>> = UpdateFn<TProps> | UpdateFn<TProps>[] | Update<TProps>[];
60
+ /**
61
+ * Defines the accepted shapes for `CorePiece.relocate`.
62
+ */
63
+ export type Relocate = RelocateFn | RelocateFn[] | Relocate[];
37
64
  /**
38
65
  * Defines the capabilities of a `CorePiece` object recognized by the core *CollageJS* library. These capabilities are
39
66
  * used to determine how the core library should handle the piece's lifecycle, or whether a particular action or
@@ -50,18 +77,6 @@ export type CorePieceCapabilities = {
50
77
  * **πŸ’‘TIP**: Official framework adapters provide this functionality.
51
78
  */
52
79
  remountable?: boolean;
53
- /**
54
- * Indicates that the piece allows relocation of its HTML markup to a new parent without unmounting. For the best
55
- * development experience, piece objects should always strive to be relocatable.
56
- *
57
- * If `false`, `Piece` components created with official framework adapters will most likely have to ask HMR to
58
- * perform a full page reload whenever the developer changes shadow DOM options, like moving from an open to a
59
- * closed shadow root.
60
- *
61
- * If `true` **and if the framework is capable** (i. e. Svelte), the piece can be relocated without unmounting, and
62
- * HMR will be able to update the shadow root options without a full page reload.
63
- */
64
- relocatable?: boolean;
65
80
  };
66
81
  /**
67
82
  * Defines the contract that objects must follow in order to be mountable as *CollageJS* pieces (micro-frontends).
@@ -77,14 +92,58 @@ export interface CorePiece<TProps extends Record<string, any> = Record<string, a
77
92
  * while mounted in the document, and all property values must have been passed during mounting.
78
93
  */
79
94
  update?: Update<TProps>;
95
+ /**
96
+ * Either relocates the root element(s) of the piece to a new parent, or prepares the piece for relocation. This
97
+ * is optional. If not provided, the piece won't support relocation of its root element(s) to a new parent, and
98
+ * the piece will be unmounted and remounted instead in pertinent cases.
99
+ *
100
+ * ### Return Values
101
+ *
102
+ * + `done`: The relocation succeeded and was made entirely by the piece.
103
+ * + `unsupported`: The piece does not support relocation.
104
+ * + `supported`: The piece supports relocation, but the relocation must be performed by the caller.
105
+ *
106
+ * Additionally, the values `done` and `supported` can be returned as a tuple with a rollback function that will be
107
+ * called if the relocation chain fails after this function has returned `done` or `supported`.
108
+ *
109
+ * ### Rollback Functions
110
+ *
111
+ * If a relocation function returns a rollback function, it will be called if any of the following occurs:
112
+ *
113
+ * + Another relocation function returns `unsupported` after this one returned `done` or `supported`.
114
+ * + Another relocation function throws an error after this one returned `done` or `supported`.
115
+ *
116
+ * ### When Using Multiple Relocation Functions
117
+ *
118
+ * If multiple relocation functions are provided, they will be called in order:
119
+ *
120
+ * + If the first of them returns `unsupported`, the relocation process will stop.
121
+ * + If all of them return `done`, the relocation is considered successful.
122
+ * + If any of them returns `'supported'`, the remaining functions are still called, but the caller will
123
+ * run its relocation process after all functions have been called, even if they return `'done'`.
124
+ * + If any of them returns `unsupported` after one or more of them returned `'supported'` or `done` without a
125
+ * rollback function, the piece's state is considered inconsistent and an error will be thrown.
126
+ */
127
+ relocate?: Relocate;
80
128
  /**
81
129
  * Declares the capabilities of the piece. This is optional. If not provided, the piece will be assumed to have no
82
- * capabilities, and the core library will treat it as a simple piece that can be mounted once and unmounted once, and
83
- * that cannot be relocated or re-mounted.
130
+ * capabilities.
131
+ *
132
+ * ### Notable Exception
133
+ *
134
+ * The library defines the `remountable` capability, which is informative only. The core library cannot enforce
135
+ * this capability, so `CorePiece` developers should use the `preventRemount()` function (or equivalent) to throw an
136
+ * error if the piece is mounted more than once for pieces that cannot withstand multiple mountings.
137
+ *
138
+ * Official framework adapters query the value of `capabilities.remountable` and act accordingly to their best
139
+ * ability, and at least emit a warning if a non-remountable piece is mounted more than once.
84
140
  *
85
- * **πŸ’‘TIP**: Always try to at least create pieces that are relocatable by not storing the original target element in
86
- * the piece's state. Instead, just use the piece's root element's `parentElement` property to determine the
87
- * current parent element.
141
+ * Only the author of a `CorePiece` object can guarantee that the piece is remountable. We encourage developers to
142
+ * be explicit about this capability when creating `CorePiece` objects.
143
+ *
144
+ * > ℹ️ **NOTE:** Official framework adapters are free to choose which default value for `capabilities.remountable`
145
+ * > they will use while creating `CorePiece` objects when the property is not provided in order to maximize
146
+ * > framework feature usage (perhaps a framework can guarantee component state automatically, or perhaps it cannot).
88
147
  */
89
148
  readonly capabilities?: CorePieceCapabilities & TCap;
90
149
  }
@@ -100,6 +159,10 @@ export interface MountedPiece<TProps extends Record<string, any> = Record<string
100
159
  * Function used to unmount the `CorePiece` object.
101
160
  */
102
161
  unmount: UnmountFn;
162
+ /**
163
+ * Function used to relocate the `CorePiece` object to a new parent without unmounting.
164
+ */
165
+ relocate: (source: AcceptableTarget, target: AcceptableTarget, customRelocate?: (source: AcceptableTarget, target: AcceptableTarget) => Promise<boolean>) => Promise<boolean>;
103
166
  /**
104
167
  * The version of the global `mountPiece` function that tracks mounted children so their unmounting is
105
168
  * synchronized with this piece's unmount event.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@collagejs/core",
3
- "version": "0.5.0",
3
+ "version": "0.7.0",
4
4
  "description": "Core functionality for CollageJS.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",