@esmx/router 3.0.0-rc.18 → 3.0.0-rc.20
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/LICENSE +1 -1
- package/README.md +70 -0
- package/README.zh-CN.md +70 -0
- package/dist/error.d.ts +23 -0
- package/dist/error.mjs +61 -0
- package/dist/increment-id.d.ts +7 -0
- package/dist/increment-id.mjs +11 -0
- package/dist/index.d.ts +5 -3
- package/dist/index.mjs +14 -3
- package/dist/index.test.mjs +8 -0
- package/dist/location.d.ts +15 -0
- package/dist/location.mjs +53 -0
- package/dist/location.test.d.ts +8 -0
- package/dist/location.test.mjs +370 -0
- package/dist/matcher.d.ts +3 -0
- package/dist/matcher.mjs +44 -0
- package/dist/matcher.test.mjs +1492 -0
- package/dist/micro-app.d.ts +18 -0
- package/dist/micro-app.dom.test.d.ts +1 -0
- package/dist/micro-app.dom.test.mjs +532 -0
- package/dist/micro-app.mjs +80 -0
- package/dist/navigation.d.ts +43 -0
- package/dist/navigation.mjs +143 -0
- package/dist/navigation.test.d.ts +1 -0
- package/dist/navigation.test.mjs +681 -0
- package/dist/options.d.ts +4 -0
- package/dist/options.mjs +88 -0
- package/dist/route-task.d.ts +40 -0
- package/dist/route-task.mjs +75 -0
- package/dist/route-task.test.d.ts +1 -0
- package/dist/route-task.test.mjs +673 -0
- package/dist/route-transition.d.ts +53 -0
- package/dist/route-transition.mjs +307 -0
- package/dist/route-transition.test.d.ts +1 -0
- package/dist/route-transition.test.mjs +146 -0
- package/dist/route.d.ts +72 -0
- package/dist/route.mjs +194 -0
- package/dist/route.test.d.ts +1 -0
- package/dist/route.test.mjs +1664 -0
- package/dist/router-back.test.d.ts +1 -0
- package/dist/router-back.test.mjs +361 -0
- package/dist/router-forward.test.d.ts +1 -0
- package/dist/router-forward.test.mjs +376 -0
- package/dist/router-go.test.d.ts +1 -0
- package/dist/router-go.test.mjs +73 -0
- package/dist/router-guards-cleanup.test.d.ts +1 -0
- package/dist/router-guards-cleanup.test.mjs +437 -0
- package/dist/router-link.d.ts +10 -0
- package/dist/router-link.mjs +126 -0
- package/dist/router-push.test.d.ts +1 -0
- package/dist/router-push.test.mjs +115 -0
- package/dist/router-replace.test.d.ts +1 -0
- package/dist/router-replace.test.mjs +114 -0
- package/dist/router-resolve.test.d.ts +1 -0
- package/dist/router-resolve.test.mjs +393 -0
- package/dist/router-restart-app.dom.test.d.ts +1 -0
- package/dist/router-restart-app.dom.test.mjs +616 -0
- package/dist/router-window-navigation.test.d.ts +1 -0
- package/dist/router-window-navigation.test.mjs +359 -0
- package/dist/router.d.ts +109 -102
- package/dist/router.mjs +260 -361
- package/dist/types.d.ts +246 -0
- package/dist/types.mjs +18 -0
- package/dist/util.d.ts +26 -0
- package/dist/util.mjs +53 -0
- package/dist/util.test.d.ts +1 -0
- package/dist/util.test.mjs +1020 -0
- package/package.json +10 -13
- package/src/error.ts +84 -0
- package/src/increment-id.ts +12 -0
- package/src/index.test.ts +9 -0
- package/src/index.ts +54 -3
- package/src/location.test.ts +406 -0
- package/src/location.ts +96 -0
- package/src/matcher.test.ts +1685 -0
- package/src/matcher.ts +59 -0
- package/src/micro-app.dom.test.ts +708 -0
- package/src/micro-app.ts +101 -0
- package/src/navigation.test.ts +858 -0
- package/src/navigation.ts +195 -0
- package/src/options.ts +131 -0
- package/src/route-task.test.ts +901 -0
- package/src/route-task.ts +105 -0
- package/src/route-transition.test.ts +178 -0
- package/src/route-transition.ts +425 -0
- package/src/route.test.ts +2014 -0
- package/src/route.ts +308 -0
- package/src/router-back.test.ts +487 -0
- package/src/router-forward.test.ts +506 -0
- package/src/router-go.test.ts +91 -0
- package/src/router-guards-cleanup.test.ts +595 -0
- package/src/router-link.ts +235 -0
- package/src/router-push.test.ts +140 -0
- package/src/router-replace.test.ts +139 -0
- package/src/router-resolve.test.ts +475 -0
- package/src/router-restart-app.dom.test.ts +783 -0
- package/src/router-window-navigation.test.ts +457 -0
- package/src/router.ts +289 -470
- package/src/types.ts +341 -0
- package/src/util.test.ts +1262 -0
- package/src/util.ts +116 -0
- package/dist/history/abstract.d.ts +0 -29
- package/dist/history/abstract.mjs +0 -107
- package/dist/history/base.d.ts +0 -79
- package/dist/history/base.mjs +0 -275
- package/dist/history/html.d.ts +0 -30
- package/dist/history/html.mjs +0 -183
- package/dist/history/index.d.ts +0 -7
- package/dist/history/index.mjs +0 -16
- package/dist/matcher/create-matcher.d.ts +0 -5
- package/dist/matcher/create-matcher.mjs +0 -218
- package/dist/matcher/create-matcher.spec.mjs +0 -0
- package/dist/matcher/index.d.ts +0 -1
- package/dist/matcher/index.mjs +0 -1
- package/dist/task-pipe/index.d.ts +0 -1
- package/dist/task-pipe/index.mjs +0 -1
- package/dist/task-pipe/task.d.ts +0 -30
- package/dist/task-pipe/task.mjs +0 -66
- package/dist/types/index.d.ts +0 -694
- package/dist/types/index.mjs +0 -6
- package/dist/utils/bom.d.ts +0 -5
- package/dist/utils/bom.mjs +0 -10
- package/dist/utils/encoding.d.ts +0 -48
- package/dist/utils/encoding.mjs +0 -44
- package/dist/utils/guards.d.ts +0 -9
- package/dist/utils/guards.mjs +0 -12
- package/dist/utils/index.d.ts +0 -7
- package/dist/utils/index.mjs +0 -27
- package/dist/utils/path.d.ts +0 -60
- package/dist/utils/path.mjs +0 -282
- package/dist/utils/path.spec.mjs +0 -27
- package/dist/utils/scroll.d.ts +0 -25
- package/dist/utils/scroll.mjs +0 -59
- package/dist/utils/utils.d.ts +0 -16
- package/dist/utils/utils.mjs +0 -11
- package/dist/utils/warn.d.ts +0 -2
- package/dist/utils/warn.mjs +0 -12
- package/src/history/abstract.ts +0 -149
- package/src/history/base.ts +0 -408
- package/src/history/html.ts +0 -228
- package/src/history/index.ts +0 -20
- package/src/matcher/create-matcher.spec.ts +0 -3
- package/src/matcher/create-matcher.ts +0 -292
- package/src/matcher/index.ts +0 -1
- package/src/task-pipe/index.ts +0 -1
- package/src/task-pipe/task.ts +0 -97
- package/src/types/index.ts +0 -858
- package/src/utils/bom.ts +0 -14
- package/src/utils/encoding.ts +0 -153
- package/src/utils/guards.ts +0 -25
- package/src/utils/index.ts +0 -27
- package/src/utils/path.spec.ts +0 -32
- package/src/utils/path.ts +0 -418
- package/src/utils/scroll.ts +0 -120
- package/src/utils/utils.ts +0 -30
- package/src/utils/warn.ts +0 -13
- /package/dist/{matcher/create-matcher.spec.d.ts → index.test.d.ts} +0 -0
- /package/dist/{utils/path.spec.d.ts → matcher.test.d.ts} +0 -0
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { Router } from './router';
|
|
2
|
+
import type { RouterMicroAppOptions } from './types';
|
|
3
|
+
/**
|
|
4
|
+
* Resolves the root container element.
|
|
5
|
+
* Supports a DOM selector string or a direct HTMLElement.
|
|
6
|
+
*
|
|
7
|
+
* @param rootConfig - The root container configuration, can be a selector string or an HTMLElement.
|
|
8
|
+
* @returns The resolved HTMLElement.
|
|
9
|
+
*/
|
|
10
|
+
export declare function resolveRootElement(rootConfig?: string | HTMLElement): HTMLElement;
|
|
11
|
+
export declare class MicroApp {
|
|
12
|
+
app: RouterMicroAppOptions | null;
|
|
13
|
+
root: HTMLElement | null;
|
|
14
|
+
private _factory;
|
|
15
|
+
_update(router: Router, force?: boolean): void;
|
|
16
|
+
private _getNextFactory;
|
|
17
|
+
destroy(): void;
|
|
18
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,532 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { MicroApp, resolveRootElement } from "./micro-app.mjs";
|
|
3
|
+
import { parsedOptions } from "./options.mjs";
|
|
4
|
+
import { Route } from "./route.mjs";
|
|
5
|
+
import { RouteType, RouterMode } from "./types.mjs";
|
|
6
|
+
const createMockParsedConfig = (app) => ({
|
|
7
|
+
path: "/test",
|
|
8
|
+
compilePath: "/test",
|
|
9
|
+
children: [],
|
|
10
|
+
match: vi.fn(),
|
|
11
|
+
compile: vi.fn(),
|
|
12
|
+
meta: {},
|
|
13
|
+
app
|
|
14
|
+
});
|
|
15
|
+
const createMockRouter = (overrides = {}) => {
|
|
16
|
+
var _a;
|
|
17
|
+
const baseOptions = {
|
|
18
|
+
root: overrides.root || "#test-router",
|
|
19
|
+
context: {},
|
|
20
|
+
routes: [],
|
|
21
|
+
mode: RouterMode.memory,
|
|
22
|
+
base: new URL("https://example.com/"),
|
|
23
|
+
req: null,
|
|
24
|
+
res: null,
|
|
25
|
+
apps: ((_a = overrides.options) == null ? void 0 : _a.apps) || {},
|
|
26
|
+
normalizeURL: (url) => url,
|
|
27
|
+
fallback: () => {
|
|
28
|
+
},
|
|
29
|
+
rootStyle: false,
|
|
30
|
+
handleBackBoundary: () => {
|
|
31
|
+
},
|
|
32
|
+
handleLayerClose: () => {
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
const mockParsedOptions = {
|
|
36
|
+
...parsedOptions(baseOptions),
|
|
37
|
+
...overrides.parsedOptions
|
|
38
|
+
};
|
|
39
|
+
if (overrides.matched) {
|
|
40
|
+
const customMatched = overrides.matched.map(
|
|
41
|
+
(item) => createMockParsedConfig(item.app || "test-app")
|
|
42
|
+
);
|
|
43
|
+
mockParsedOptions.matcher = () => ({
|
|
44
|
+
matches: customMatched,
|
|
45
|
+
params: {}
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
const mockRoute = new Route({
|
|
49
|
+
options: mockParsedOptions,
|
|
50
|
+
toType: RouteType.push,
|
|
51
|
+
toInput: "/test"
|
|
52
|
+
});
|
|
53
|
+
return {
|
|
54
|
+
root: overrides.root || "#test-router",
|
|
55
|
+
route: mockRoute,
|
|
56
|
+
options: overrides.options || {},
|
|
57
|
+
parsedOptions: mockParsedOptions
|
|
58
|
+
};
|
|
59
|
+
};
|
|
60
|
+
const createMockApp = () => ({
|
|
61
|
+
mount: vi.fn(),
|
|
62
|
+
unmount: vi.fn(),
|
|
63
|
+
renderToString: vi.fn().mockResolvedValue("<div>rendered</div>")
|
|
64
|
+
});
|
|
65
|
+
describe("resolveRootElement", () => {
|
|
66
|
+
afterEach(() => {
|
|
67
|
+
vi.clearAllMocks();
|
|
68
|
+
document.body.innerHTML = "";
|
|
69
|
+
});
|
|
70
|
+
describe("Basic functionality tests", () => {
|
|
71
|
+
it("should return a div element when the parameter is empty", () => {
|
|
72
|
+
const result1 = resolveRootElement();
|
|
73
|
+
expect(result1).toBeInstanceOf(HTMLElement);
|
|
74
|
+
expect(result1.tagName).toBe("DIV");
|
|
75
|
+
const result2 = resolveRootElement(void 0);
|
|
76
|
+
expect(result2).toBeInstanceOf(HTMLElement);
|
|
77
|
+
expect(result2.tagName).toBe("DIV");
|
|
78
|
+
});
|
|
79
|
+
it("should correctly handle a directly passed HTMLElement", () => {
|
|
80
|
+
const element = document.createElement("div");
|
|
81
|
+
element.id = "test-element";
|
|
82
|
+
const result = resolveRootElement(element);
|
|
83
|
+
expect(result).toBe(element);
|
|
84
|
+
expect(result.id).toBe("test-element");
|
|
85
|
+
});
|
|
86
|
+
it("should correctly handle a string selector", () => {
|
|
87
|
+
const existingElement = document.createElement("div");
|
|
88
|
+
existingElement.id = "existing-element";
|
|
89
|
+
document.body.appendChild(existingElement);
|
|
90
|
+
const result = resolveRootElement("#existing-element");
|
|
91
|
+
expect(result).toBe(existingElement);
|
|
92
|
+
});
|
|
93
|
+
it("should create a new element when not found", () => {
|
|
94
|
+
const result = resolveRootElement("#non-existent");
|
|
95
|
+
expect(result).toBeInstanceOf(HTMLElement);
|
|
96
|
+
expect(result.tagName).toBe("DIV");
|
|
97
|
+
expect(result.id).toBe("");
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
describe("Selector type tests", () => {
|
|
101
|
+
it("should handle ID selectors", () => {
|
|
102
|
+
const existingElement = document.createElement("div");
|
|
103
|
+
existingElement.id = "app";
|
|
104
|
+
document.body.appendChild(existingElement);
|
|
105
|
+
const result = resolveRootElement("#app");
|
|
106
|
+
expect(result).toBeInstanceOf(HTMLElement);
|
|
107
|
+
expect(result.id).toBe("app");
|
|
108
|
+
const newResult = resolveRootElement("#new-app");
|
|
109
|
+
expect(newResult).toBeInstanceOf(HTMLElement);
|
|
110
|
+
expect(newResult.tagName).toBe("DIV");
|
|
111
|
+
expect(newResult.id).toBe("");
|
|
112
|
+
});
|
|
113
|
+
it("should handle class selectors", () => {
|
|
114
|
+
const element = document.createElement("div");
|
|
115
|
+
element.className = "app-container";
|
|
116
|
+
document.body.appendChild(element);
|
|
117
|
+
const result = resolveRootElement(".app-container");
|
|
118
|
+
expect(result).toBe(element);
|
|
119
|
+
});
|
|
120
|
+
it("should handle attribute selectors", () => {
|
|
121
|
+
const element = document.createElement("div");
|
|
122
|
+
element.setAttribute("data-app", "main");
|
|
123
|
+
document.body.appendChild(element);
|
|
124
|
+
const result = resolveRootElement('[data-app="main"]');
|
|
125
|
+
expect(result).toBe(element);
|
|
126
|
+
});
|
|
127
|
+
it("should handle tag selectors", () => {
|
|
128
|
+
const element = document.createElement("main");
|
|
129
|
+
document.body.appendChild(element);
|
|
130
|
+
const result = resolveRootElement("main");
|
|
131
|
+
expect(result).toBe(element);
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
describe("Edge case tests", () => {
|
|
135
|
+
it("should handle complex selectors", () => {
|
|
136
|
+
const container = document.createElement("div");
|
|
137
|
+
container.className = "container";
|
|
138
|
+
const app = document.createElement("div");
|
|
139
|
+
app.id = "app";
|
|
140
|
+
container.appendChild(app);
|
|
141
|
+
document.body.appendChild(container);
|
|
142
|
+
const result = resolveRootElement(".container #app");
|
|
143
|
+
expect(result).toBe(app);
|
|
144
|
+
});
|
|
145
|
+
it("should return the first matching element", () => {
|
|
146
|
+
const element1 = document.createElement("div");
|
|
147
|
+
element1.className = "multiple";
|
|
148
|
+
element1.textContent = "first";
|
|
149
|
+
const element2 = document.createElement("div");
|
|
150
|
+
element2.className = "multiple";
|
|
151
|
+
element2.textContent = "second";
|
|
152
|
+
document.body.appendChild(element1);
|
|
153
|
+
document.body.appendChild(element2);
|
|
154
|
+
const result = resolveRootElement(".multiple");
|
|
155
|
+
expect(result).toBe(element1);
|
|
156
|
+
});
|
|
157
|
+
it("should handle non-string, non-HTMLElement inputs", () => {
|
|
158
|
+
const result1 = resolveRootElement(123);
|
|
159
|
+
expect(result1).toBeInstanceOf(HTMLElement);
|
|
160
|
+
expect(result1.tagName).toBe("DIV");
|
|
161
|
+
const result2 = resolveRootElement({});
|
|
162
|
+
expect(result2).toBeInstanceOf(HTMLElement);
|
|
163
|
+
expect(result2.tagName).toBe("DIV");
|
|
164
|
+
const result3 = resolveRootElement([]);
|
|
165
|
+
expect(result3).toBeInstanceOf(HTMLElement);
|
|
166
|
+
expect(result3.tagName).toBe("DIV");
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
describe("Type safety tests", () => {
|
|
170
|
+
it("should return any type of element found", () => {
|
|
171
|
+
const svg = document.createElementNS(
|
|
172
|
+
"http://www.w3.org/2000/svg",
|
|
173
|
+
"svg"
|
|
174
|
+
);
|
|
175
|
+
svg.id = "svg-element";
|
|
176
|
+
document.body.appendChild(svg);
|
|
177
|
+
const result = resolveRootElement("#svg-element");
|
|
178
|
+
expect(result).toBe(svg);
|
|
179
|
+
expect(result).toBeInstanceOf(SVGElement);
|
|
180
|
+
});
|
|
181
|
+
it("should handle non-element nodes like text nodes", () => {
|
|
182
|
+
const result = resolveRootElement("#non-existent-text");
|
|
183
|
+
expect(result).toBeInstanceOf(HTMLElement);
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
describe("MicroApp", () => {
|
|
188
|
+
let microApp;
|
|
189
|
+
beforeEach(() => {
|
|
190
|
+
microApp = new MicroApp();
|
|
191
|
+
});
|
|
192
|
+
afterEach(() => {
|
|
193
|
+
vi.clearAllMocks();
|
|
194
|
+
document.body.innerHTML = "";
|
|
195
|
+
});
|
|
196
|
+
describe("Initial state", () => {
|
|
197
|
+
it("should initialize with a null state", () => {
|
|
198
|
+
expect(microApp.app).toBeNull();
|
|
199
|
+
expect(microApp.root).toBeNull();
|
|
200
|
+
expect(microApp._factory).toBeNull();
|
|
201
|
+
});
|
|
202
|
+
});
|
|
203
|
+
describe("_getNextFactory", () => {
|
|
204
|
+
it("should get the app name from route match and return the corresponding factory", () => {
|
|
205
|
+
const mockFactory = vi.fn();
|
|
206
|
+
const router = createMockRouter({
|
|
207
|
+
matched: [{ app: "vue-app" }],
|
|
208
|
+
options: { apps: { "vue-app": mockFactory } }
|
|
209
|
+
});
|
|
210
|
+
const factory = microApp._getNextFactory(router);
|
|
211
|
+
expect(factory).toBe(mockFactory);
|
|
212
|
+
});
|
|
213
|
+
it("should return null if the app name does not exist", () => {
|
|
214
|
+
const router = createMockRouter({
|
|
215
|
+
matched: [{ app: "non-existent-app" }],
|
|
216
|
+
options: { apps: { "vue-app": vi.fn() } }
|
|
217
|
+
});
|
|
218
|
+
const factory = microApp._getNextFactory(router);
|
|
219
|
+
expect(factory).toBeNull();
|
|
220
|
+
});
|
|
221
|
+
it("should handle function-type apps in the match result", () => {
|
|
222
|
+
const mockFactory = vi.fn();
|
|
223
|
+
const router = createMockRouter({
|
|
224
|
+
matched: [{ app: mockFactory }]
|
|
225
|
+
});
|
|
226
|
+
const factory = microApp._getNextFactory(router);
|
|
227
|
+
expect(factory).toBe(mockFactory);
|
|
228
|
+
});
|
|
229
|
+
it("should handle options.apps being a function", () => {
|
|
230
|
+
const mockFactory = vi.fn();
|
|
231
|
+
const router = createMockRouter({
|
|
232
|
+
matched: [{ app: "any-app" }],
|
|
233
|
+
options: { apps: mockFactory }
|
|
234
|
+
});
|
|
235
|
+
const factory = microApp._getNextFactory(router);
|
|
236
|
+
expect(factory).toBe(mockFactory);
|
|
237
|
+
});
|
|
238
|
+
it("should return null when there are no match results", () => {
|
|
239
|
+
const router = createMockRouter({
|
|
240
|
+
matched: []
|
|
241
|
+
});
|
|
242
|
+
const factory = microApp._getNextFactory(router);
|
|
243
|
+
expect(factory).toBeNull();
|
|
244
|
+
});
|
|
245
|
+
it("should return null when options.apps is an empty object", () => {
|
|
246
|
+
const router = createMockRouter({
|
|
247
|
+
matched: [{ app: "test-app" }],
|
|
248
|
+
options: { apps: {} }
|
|
249
|
+
});
|
|
250
|
+
const factory = microApp._getNextFactory(router);
|
|
251
|
+
expect(factory).toBeNull();
|
|
252
|
+
});
|
|
253
|
+
});
|
|
254
|
+
describe("_update method", () => {
|
|
255
|
+
it("should update the factory and create the application", () => {
|
|
256
|
+
const mockFactory = vi.fn().mockReturnValue(createMockApp());
|
|
257
|
+
const router = createMockRouter({
|
|
258
|
+
matched: [{ app: "test-app" }],
|
|
259
|
+
options: { apps: { "test-app": mockFactory } }
|
|
260
|
+
});
|
|
261
|
+
microApp._update(router);
|
|
262
|
+
expect(microApp._factory).toBe(mockFactory);
|
|
263
|
+
expect(mockFactory).toHaveBeenCalledWith(router);
|
|
264
|
+
expect(microApp.app).not.toBeNull();
|
|
265
|
+
expect(microApp.root).not.toBeNull();
|
|
266
|
+
});
|
|
267
|
+
it("should skip update if factory has not changed and force=false", () => {
|
|
268
|
+
const mockFactory = vi.fn().mockReturnValue(createMockApp());
|
|
269
|
+
const router = createMockRouter({
|
|
270
|
+
matched: [{ app: "test-app" }],
|
|
271
|
+
options: { apps: { "test-app": mockFactory } }
|
|
272
|
+
});
|
|
273
|
+
microApp._update(router);
|
|
274
|
+
expect(mockFactory).toHaveBeenCalledTimes(1);
|
|
275
|
+
microApp._update(router);
|
|
276
|
+
expect(mockFactory).toHaveBeenCalledTimes(1);
|
|
277
|
+
});
|
|
278
|
+
it("should force update when force=true", () => {
|
|
279
|
+
const mockFactory = vi.fn().mockReturnValue(createMockApp());
|
|
280
|
+
const router = createMockRouter({
|
|
281
|
+
matched: [{ app: "test-app" }],
|
|
282
|
+
options: { apps: { "test-app": mockFactory } }
|
|
283
|
+
});
|
|
284
|
+
microApp._update(router);
|
|
285
|
+
expect(mockFactory).toHaveBeenCalledTimes(1);
|
|
286
|
+
microApp._update(router, true);
|
|
287
|
+
expect(mockFactory).toHaveBeenCalledTimes(2);
|
|
288
|
+
});
|
|
289
|
+
it("should set the application to null if there is no factory", () => {
|
|
290
|
+
const router = createMockRouter({
|
|
291
|
+
matched: [{ app: "non-existent" }],
|
|
292
|
+
options: { apps: {} }
|
|
293
|
+
});
|
|
294
|
+
microApp._update(router);
|
|
295
|
+
expect(microApp.app).toBeNull();
|
|
296
|
+
expect(microApp._factory).toBeNull();
|
|
297
|
+
});
|
|
298
|
+
it("should create a new root element and mount the application", () => {
|
|
299
|
+
const mockApp = createMockApp();
|
|
300
|
+
const mockFactory = vi.fn().mockReturnValue(mockApp);
|
|
301
|
+
const router = createMockRouter({
|
|
302
|
+
root: "#test-router",
|
|
303
|
+
matched: [{ app: "test-app" }],
|
|
304
|
+
options: { apps: { "test-app": mockFactory } }
|
|
305
|
+
});
|
|
306
|
+
microApp._update(router);
|
|
307
|
+
expect(microApp.root).toBeInstanceOf(HTMLElement);
|
|
308
|
+
expect(microApp.root.tagName).toBe("DIV");
|
|
309
|
+
expect(mockApp.mount).toHaveBeenCalledWith(microApp.root);
|
|
310
|
+
expect(document.body.contains(microApp.root)).toBe(true);
|
|
311
|
+
});
|
|
312
|
+
it("should use an existing root element", () => {
|
|
313
|
+
const existingElement = document.createElement("div");
|
|
314
|
+
existingElement.id = "test-router";
|
|
315
|
+
document.body.appendChild(existingElement);
|
|
316
|
+
const mockApp = createMockApp();
|
|
317
|
+
const mockFactory = vi.fn().mockReturnValue(mockApp);
|
|
318
|
+
const router = createMockRouter({
|
|
319
|
+
root: "#test-router",
|
|
320
|
+
matched: [{ app: "test-app" }],
|
|
321
|
+
options: { apps: { "test-app": mockFactory } }
|
|
322
|
+
});
|
|
323
|
+
microApp._update(router);
|
|
324
|
+
expect(microApp.root).toBe(existingElement);
|
|
325
|
+
expect(mockApp.mount).toHaveBeenCalledWith(existingElement);
|
|
326
|
+
});
|
|
327
|
+
it("should use the already set root element", () => {
|
|
328
|
+
const existingRoot = document.createElement("div");
|
|
329
|
+
existingRoot.id = "existing-root";
|
|
330
|
+
document.body.appendChild(existingRoot);
|
|
331
|
+
microApp.root = existingRoot;
|
|
332
|
+
const mockApp = createMockApp();
|
|
333
|
+
const mockFactory = vi.fn().mockReturnValue(mockApp);
|
|
334
|
+
const router = createMockRouter({
|
|
335
|
+
matched: [{ app: "test-app" }],
|
|
336
|
+
options: { apps: { "test-app": mockFactory } }
|
|
337
|
+
});
|
|
338
|
+
microApp._update(router);
|
|
339
|
+
expect(microApp.root).toBe(existingRoot);
|
|
340
|
+
expect(mockApp.mount).toHaveBeenCalledWith(existingRoot);
|
|
341
|
+
});
|
|
342
|
+
it("should apply rootStyle styles", () => {
|
|
343
|
+
const mockApp = createMockApp();
|
|
344
|
+
const mockFactory = vi.fn().mockReturnValue(mockApp);
|
|
345
|
+
const router = createMockRouter({
|
|
346
|
+
matched: [{ app: "test-app" }],
|
|
347
|
+
options: { apps: { "test-app": mockFactory } },
|
|
348
|
+
parsedOptions: {
|
|
349
|
+
rootStyle: { color: "red", fontSize: "16px" }
|
|
350
|
+
}
|
|
351
|
+
});
|
|
352
|
+
microApp._update(router);
|
|
353
|
+
expect(microApp.root.style.color).toBe("red");
|
|
354
|
+
expect(microApp.root.style.fontSize).toBe("16px");
|
|
355
|
+
});
|
|
356
|
+
it("should unmount the old application if it exists", () => {
|
|
357
|
+
const oldApp = createMockApp();
|
|
358
|
+
const newApp = createMockApp();
|
|
359
|
+
const oldFactory = vi.fn().mockReturnValue(oldApp);
|
|
360
|
+
const newFactory = vi.fn().mockReturnValue(newApp);
|
|
361
|
+
const router1 = createMockRouter({
|
|
362
|
+
matched: [{ app: "old-app" }],
|
|
363
|
+
options: { apps: { "old-app": oldFactory } }
|
|
364
|
+
});
|
|
365
|
+
microApp._update(router1);
|
|
366
|
+
expect(microApp.app).toBe(oldApp);
|
|
367
|
+
expect(oldApp.unmount).not.toHaveBeenCalled();
|
|
368
|
+
const router2 = createMockRouter({
|
|
369
|
+
matched: [{ app: "new-app" }],
|
|
370
|
+
options: { apps: { "new-app": newFactory } }
|
|
371
|
+
});
|
|
372
|
+
microApp._update(router2);
|
|
373
|
+
expect(oldApp.unmount).toHaveBeenCalled();
|
|
374
|
+
expect(microApp.app).toBe(newApp);
|
|
375
|
+
});
|
|
376
|
+
it("should append the root element to the body if not in DOM", () => {
|
|
377
|
+
const mockApp = createMockApp();
|
|
378
|
+
const mockFactory = vi.fn().mockReturnValue(mockApp);
|
|
379
|
+
const router = createMockRouter({
|
|
380
|
+
matched: [{ app: "test-app" }],
|
|
381
|
+
options: { apps: { "test-app": mockFactory } }
|
|
382
|
+
});
|
|
383
|
+
microApp._update(router);
|
|
384
|
+
expect(document.body.contains(microApp.root)).toBe(true);
|
|
385
|
+
});
|
|
386
|
+
it("should not re-append an element already in the DOM", () => {
|
|
387
|
+
const existingElement = document.createElement("div");
|
|
388
|
+
existingElement.id = "test-router";
|
|
389
|
+
document.body.appendChild(existingElement);
|
|
390
|
+
const mockApp = createMockApp();
|
|
391
|
+
const mockFactory = vi.fn().mockReturnValue(mockApp);
|
|
392
|
+
const router = createMockRouter({
|
|
393
|
+
root: "#test-router",
|
|
394
|
+
matched: [{ app: "test-app" }],
|
|
395
|
+
options: { apps: { "test-app": mockFactory } }
|
|
396
|
+
});
|
|
397
|
+
const initialChildCount = document.body.children.length;
|
|
398
|
+
microApp._update(router);
|
|
399
|
+
expect(document.body.children.length).toBe(initialChildCount);
|
|
400
|
+
expect(microApp.root).toBe(existingElement);
|
|
401
|
+
});
|
|
402
|
+
it("should handle the case where the factory function is null", () => {
|
|
403
|
+
const router = createMockRouter({
|
|
404
|
+
matched: [],
|
|
405
|
+
options: { apps: {} }
|
|
406
|
+
});
|
|
407
|
+
microApp._update(router, true);
|
|
408
|
+
expect(microApp.app).toBeNull();
|
|
409
|
+
expect(microApp._factory).toBeNull();
|
|
410
|
+
});
|
|
411
|
+
});
|
|
412
|
+
describe("destroy method", () => {
|
|
413
|
+
it("should destroy the application and clean up the state", () => {
|
|
414
|
+
const mockApp = createMockApp();
|
|
415
|
+
const mockRoot = document.createElement("div");
|
|
416
|
+
document.body.appendChild(mockRoot);
|
|
417
|
+
microApp.app = mockApp;
|
|
418
|
+
microApp.root = mockRoot;
|
|
419
|
+
microApp._factory = vi.fn();
|
|
420
|
+
microApp.destroy();
|
|
421
|
+
expect(mockApp.unmount).toHaveBeenCalled();
|
|
422
|
+
expect(document.body.contains(mockRoot)).toBe(false);
|
|
423
|
+
expect(microApp.app).toBeNull();
|
|
424
|
+
expect(microApp.root).toBeNull();
|
|
425
|
+
expect(microApp._factory).toBeNull();
|
|
426
|
+
});
|
|
427
|
+
it("should safely handle empty state", () => {
|
|
428
|
+
expect(() => microApp.destroy()).not.toThrow();
|
|
429
|
+
expect(microApp.app).toBeNull();
|
|
430
|
+
expect(microApp.root).toBeNull();
|
|
431
|
+
expect(microApp._factory).toBeNull();
|
|
432
|
+
});
|
|
433
|
+
it("should handle partial state", () => {
|
|
434
|
+
const mockApp = createMockApp();
|
|
435
|
+
microApp.app = mockApp;
|
|
436
|
+
expect(() => microApp.destroy()).not.toThrow();
|
|
437
|
+
expect(mockApp.unmount).toHaveBeenCalled();
|
|
438
|
+
expect(microApp.app).toBeNull();
|
|
439
|
+
});
|
|
440
|
+
});
|
|
441
|
+
describe("integration tests", () => {
|
|
442
|
+
it("should fully handle the application lifecycle", () => {
|
|
443
|
+
const mockApp1 = createMockApp();
|
|
444
|
+
const mockApp2 = createMockApp();
|
|
445
|
+
const factory1 = vi.fn().mockReturnValue(mockApp1);
|
|
446
|
+
const factory2 = vi.fn().mockReturnValue(mockApp2);
|
|
447
|
+
const router1 = createMockRouter({
|
|
448
|
+
root: "#app1",
|
|
449
|
+
matched: [{ app: "app1" }],
|
|
450
|
+
options: { apps: { app1: factory1 } }
|
|
451
|
+
});
|
|
452
|
+
microApp._update(router1);
|
|
453
|
+
expect(factory1).toHaveBeenCalledWith(router1);
|
|
454
|
+
expect(mockApp1.mount).toHaveBeenCalledWith(microApp.root);
|
|
455
|
+
expect(microApp.app).toBe(mockApp1);
|
|
456
|
+
const router2 = createMockRouter({
|
|
457
|
+
root: "#app2",
|
|
458
|
+
matched: [{ app: "app2" }],
|
|
459
|
+
options: { apps: { app2: factory2 } }
|
|
460
|
+
});
|
|
461
|
+
microApp._update(router2);
|
|
462
|
+
expect(mockApp1.unmount).toHaveBeenCalled();
|
|
463
|
+
expect(factory2).toHaveBeenCalledWith(router2);
|
|
464
|
+
expect(mockApp2.mount).toHaveBeenCalledWith(microApp.root);
|
|
465
|
+
expect(microApp.app).toBe(mockApp2);
|
|
466
|
+
microApp.destroy();
|
|
467
|
+
expect(mockApp2.unmount).toHaveBeenCalled();
|
|
468
|
+
expect(document.body.contains(microApp.root)).toBe(false);
|
|
469
|
+
expect(microApp.app).toBeNull();
|
|
470
|
+
expect(microApp.root).toBeNull();
|
|
471
|
+
});
|
|
472
|
+
it("should handle complex route application configuration", () => {
|
|
473
|
+
const mockApp = createMockApp();
|
|
474
|
+
const dynamicFactory = vi.fn().mockReturnValue(mockApp);
|
|
475
|
+
const router = createMockRouter({
|
|
476
|
+
matched: [{ app: dynamicFactory }],
|
|
477
|
+
options: { apps: {} }
|
|
478
|
+
});
|
|
479
|
+
microApp._update(router);
|
|
480
|
+
expect(dynamicFactory).toHaveBeenCalledWith(router);
|
|
481
|
+
expect(mockApp.mount).toHaveBeenCalledWith(microApp.root);
|
|
482
|
+
expect(microApp.app).toBe(mockApp);
|
|
483
|
+
});
|
|
484
|
+
it("should correctly handle rootStyle being false", () => {
|
|
485
|
+
const mockApp = createMockApp();
|
|
486
|
+
const mockFactory = vi.fn().mockReturnValue(mockApp);
|
|
487
|
+
const router = createMockRouter({
|
|
488
|
+
matched: [{ app: "test-app" }],
|
|
489
|
+
options: { apps: { "test-app": mockFactory } },
|
|
490
|
+
parsedOptions: { rootStyle: false }
|
|
491
|
+
});
|
|
492
|
+
microApp._update(router);
|
|
493
|
+
expect(microApp.root.style.cssText).toBe("");
|
|
494
|
+
});
|
|
495
|
+
});
|
|
496
|
+
describe("boundary case tests", () => {
|
|
497
|
+
it("should handle the case where route.matched is an empty array", () => {
|
|
498
|
+
const router = createMockRouter({
|
|
499
|
+
matched: []
|
|
500
|
+
});
|
|
501
|
+
expect(() => microApp._update(router)).not.toThrow();
|
|
502
|
+
expect(microApp.app).toBeNull();
|
|
503
|
+
});
|
|
504
|
+
it("should handle the case where route.matched[0] does not have an app attribute", () => {
|
|
505
|
+
const router = createMockRouter({
|
|
506
|
+
matched: [{}]
|
|
507
|
+
});
|
|
508
|
+
expect(() => microApp._update(router)).not.toThrow();
|
|
509
|
+
expect(microApp.app).toBeNull();
|
|
510
|
+
});
|
|
511
|
+
it("should handle the case where the factory function returns null", () => {
|
|
512
|
+
const mockFactory = vi.fn().mockReturnValue(null);
|
|
513
|
+
const router = createMockRouter({
|
|
514
|
+
matched: [{ app: "test-app" }],
|
|
515
|
+
options: { apps: { "test-app": mockFactory } }
|
|
516
|
+
});
|
|
517
|
+
microApp._update(router);
|
|
518
|
+
expect(mockFactory).toHaveBeenCalledWith(router);
|
|
519
|
+
expect(microApp.app).toBeNull();
|
|
520
|
+
});
|
|
521
|
+
it("should handle the case where the factory function throws an exception", () => {
|
|
522
|
+
const mockFactory = vi.fn().mockImplementation(() => {
|
|
523
|
+
throw new Error("Factory error");
|
|
524
|
+
});
|
|
525
|
+
const router = createMockRouter({
|
|
526
|
+
matched: [{ app: "test-app" }],
|
|
527
|
+
options: { apps: { "test-app": mockFactory } }
|
|
528
|
+
});
|
|
529
|
+
expect(() => microApp._update(router)).toThrow("Factory error");
|
|
530
|
+
});
|
|
531
|
+
});
|
|
532
|
+
});
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { isBrowser, isPlainObject } from "./util.mjs";
|
|
2
|
+
export function resolveRootElement(rootConfig) {
|
|
3
|
+
let el = null;
|
|
4
|
+
if (rootConfig instanceof HTMLElement) {
|
|
5
|
+
el = rootConfig;
|
|
6
|
+
}
|
|
7
|
+
if (typeof rootConfig === "string" && rootConfig) {
|
|
8
|
+
try {
|
|
9
|
+
el = document.querySelector(rootConfig);
|
|
10
|
+
} catch (error) {
|
|
11
|
+
console.warn(`Failed to resolve root element: ${rootConfig}`);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
if (el === null) {
|
|
15
|
+
el = document.createElement("div");
|
|
16
|
+
}
|
|
17
|
+
return el;
|
|
18
|
+
}
|
|
19
|
+
export class MicroApp {
|
|
20
|
+
app = null;
|
|
21
|
+
root = null;
|
|
22
|
+
_factory = null;
|
|
23
|
+
_update(router, force = false) {
|
|
24
|
+
const factory = this._getNextFactory(router);
|
|
25
|
+
if (!force && factory === this._factory) {
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
const oldApp = this.app;
|
|
29
|
+
const app = factory ? factory(router) : null;
|
|
30
|
+
if (isBrowser && app) {
|
|
31
|
+
let root = this.root;
|
|
32
|
+
if (root === null) {
|
|
33
|
+
root = resolveRootElement(router.root);
|
|
34
|
+
const { rootStyle } = router.parsedOptions;
|
|
35
|
+
if (root && isPlainObject(rootStyle)) {
|
|
36
|
+
Object.assign(root.style, router.parsedOptions.rootStyle);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
if (root) {
|
|
40
|
+
app.mount(root);
|
|
41
|
+
if (root.parentNode === null) {
|
|
42
|
+
document.body.appendChild(root);
|
|
43
|
+
}
|
|
44
|
+
this.root = root;
|
|
45
|
+
}
|
|
46
|
+
if (oldApp) {
|
|
47
|
+
oldApp.unmount();
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
this.app = app;
|
|
51
|
+
this._factory = factory;
|
|
52
|
+
}
|
|
53
|
+
_getNextFactory({
|
|
54
|
+
route,
|
|
55
|
+
options
|
|
56
|
+
}) {
|
|
57
|
+
if (!route.matched || route.matched.length === 0) {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
const name = route.matched[0].app;
|
|
61
|
+
if (typeof name === "string" && options.apps && typeof options.apps === "object") {
|
|
62
|
+
return options.apps[name] || null;
|
|
63
|
+
}
|
|
64
|
+
if (typeof name === "function") {
|
|
65
|
+
return name;
|
|
66
|
+
}
|
|
67
|
+
if (typeof options.apps === "function") {
|
|
68
|
+
return options.apps;
|
|
69
|
+
}
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
destroy() {
|
|
73
|
+
var _a, _b;
|
|
74
|
+
(_a = this.app) == null ? void 0 : _a.unmount();
|
|
75
|
+
this.app = null;
|
|
76
|
+
(_b = this.root) == null ? void 0 : _b.remove();
|
|
77
|
+
this.root = null;
|
|
78
|
+
this._factory = null;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { RouteState, RouterParsedOptions } from './types';
|
|
2
|
+
type NavigationSubscribe = (url: string, state: RouteState) => void;
|
|
3
|
+
type NavigationGoResult = null | {
|
|
4
|
+
type: 'success';
|
|
5
|
+
url: string;
|
|
6
|
+
state: RouteState;
|
|
7
|
+
};
|
|
8
|
+
export declare class Navigation {
|
|
9
|
+
readonly options: RouterParsedOptions;
|
|
10
|
+
private readonly _history;
|
|
11
|
+
private readonly _unSubscribePopState;
|
|
12
|
+
private _promiseResolve;
|
|
13
|
+
constructor(options: RouterParsedOptions, onUpdated?: NavigationSubscribe);
|
|
14
|
+
get length(): number;
|
|
15
|
+
private _push;
|
|
16
|
+
private _replace;
|
|
17
|
+
push(data: any, url?: string | URL | null): RouteState;
|
|
18
|
+
replace(data: any, url?: string | URL | null): RouteState;
|
|
19
|
+
pushHistoryState(data: any, url?: string | URL | null): void;
|
|
20
|
+
replaceHistoryState(data: any, url?: string | URL | null): void;
|
|
21
|
+
go(index: number): Promise<NavigationGoResult>;
|
|
22
|
+
forward(): Promise<NavigationGoResult>;
|
|
23
|
+
back(): Promise<NavigationGoResult>;
|
|
24
|
+
destroy(): void;
|
|
25
|
+
}
|
|
26
|
+
export declare class MemoryHistory implements History {
|
|
27
|
+
private _entries;
|
|
28
|
+
private _index;
|
|
29
|
+
private get _curEntry();
|
|
30
|
+
private readonly _popStateCbs;
|
|
31
|
+
scrollRestoration: ScrollRestoration;
|
|
32
|
+
get state(): any;
|
|
33
|
+
get url(): string;
|
|
34
|
+
constructor();
|
|
35
|
+
get length(): number;
|
|
36
|
+
pushState(data: any, unused: string, url?: string | URL | null): void;
|
|
37
|
+
replaceState(data: any, unused: string, url?: string | URL | null): void;
|
|
38
|
+
back(): void;
|
|
39
|
+
forward(): void;
|
|
40
|
+
go(delta?: number): void;
|
|
41
|
+
onPopState(cb: NavigationSubscribe): () => void;
|
|
42
|
+
}
|
|
43
|
+
export {};
|