@awesomeness-js/server 1.1.10 → 1.1.11
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/build/build.js +4 -1
- package/build/postBuild.js +4 -1
- package/index.js +6 -4
- package/package.json +1 -1
- package/src/componentAndPageMemory.js +269 -0
- package/src/componentDependencies.js +9 -19
- package/src/extractUiComponentRefs.js +55 -0
- package/src/fetchPage.js +79 -4
- package/src/resolveRealCasePath.js +0 -4
- package/tests/componentAndPageMemory.test.js +171 -0
- package/tests/fetchPage.test.js +257 -0
- package/tests/fixtures/site-and-components/components/card/index.js +88 -0
- package/tests/fixtures/site-and-components/components/cardMain/index.js +86 -0
- package/tests/fixtures/site-and-components/components/cardMount/index.js +86 -0
- package/tests/fixtures/site-and-components/components/pageInit/index.js +86 -0
- package/tests/fixtures/site-and-components/components/pageScript/index.js +86 -0
- package/tests/fixtures/site-and-components/components/pageWidget/index.js +86 -0
- package/tests/fixtures/site-and-components/sites/site-a/pages/home/init.js +78 -0
- package/tests/fixtures/site-and-components/sites/site-a/pages/home/js/index.js +60 -0
- package/tests/setup.js +1 -0
- package/tsconfig.json +18 -0
- package/types/errors.d.ts +0 -0
- package/types/index.d.ts +109 -0
- package/types/src/applicationMap.d.ts +1 -0
- package/types/src/awesomenessNormalizeRequest.d.ts +27 -0
- package/types/src/brotliJsonResponse.d.ts +10 -0
- package/types/src/checkAccess.d.ts +5 -0
- package/types/src/componentAndPageMemory.d.ts +75 -0
- package/types/src/componentDependencies.d.ts +6 -0
- package/types/src/config.d.ts +7 -0
- package/types/src/extractUiComponentRefs.d.ts +5 -0
- package/types/src/fetchPage.d.ts +4 -0
- package/types/src/getConfig.d.ts +2 -0
- package/types/src/getMD.d.ts +1 -0
- package/types/src/init.d.ts +2 -0
- package/types/src/koa/attachAwesomenessRequest.d.ts +1 -0
- package/types/src/koa/cors.d.ts +1 -0
- package/types/src/koa/errorHandler.d.ts +1 -0
- package/types/src/koa/finalFormat.d.ts +1 -0
- package/types/src/koa/jsonBodyParser.d.ts +1 -0
- package/types/src/koa/routeRequest.d.ts +1 -0
- package/types/src/koa/serverUp.d.ts +1 -0
- package/types/src/koa/staticFiles.d.ts +1 -0
- package/types/src/koa/timeout.d.ts +1 -0
- package/types/src/pageInfo.d.ts +8 -0
- package/types/src/reRoute.d.ts +7 -0
- package/types/src/resolveRealCasePath.d.ts +1 -0
- package/types/src/specialPaths.d.ts +3 -0
- package/types/src/start.d.ts +1 -0
- package/types/src/validateRequest.d.ts +2 -0
- package/types/src/ws/handlers.d.ts +4 -0
- package/types/src/ws/index.d.ts +1 -0
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import path from "path";
|
|
2
|
+
import { performance } from "perf_hooks";
|
|
3
|
+
import { describe, expect, it } from "vitest";
|
|
4
|
+
|
|
5
|
+
import componentAndPageMemory, { clearComponentAndPageMemory,
|
|
6
|
+
extractUiRefsFromFileMemoized,
|
|
7
|
+
extractUiRefsMemoized,
|
|
8
|
+
getComponentAndPageMemoryStatus, } from "../src/componentAndPageMemory.js";
|
|
9
|
+
|
|
10
|
+
describe("componentAndPageMemory", () => {
|
|
11
|
+
|
|
12
|
+
const fixturesRoot = path.join(
|
|
13
|
+
process.cwd(),
|
|
14
|
+
"tests",
|
|
15
|
+
"fixtures",
|
|
16
|
+
"site-and-components"
|
|
17
|
+
);
|
|
18
|
+
const siteInitPath = path.join(
|
|
19
|
+
fixturesRoot,
|
|
20
|
+
"sites",
|
|
21
|
+
"site-a",
|
|
22
|
+
"pages",
|
|
23
|
+
"home",
|
|
24
|
+
"init.js"
|
|
25
|
+
);
|
|
26
|
+
const siteJsPath = path.join(
|
|
27
|
+
fixturesRoot,
|
|
28
|
+
"sites",
|
|
29
|
+
"site-a",
|
|
30
|
+
"pages",
|
|
31
|
+
"home",
|
|
32
|
+
"js",
|
|
33
|
+
"index.js"
|
|
34
|
+
);
|
|
35
|
+
const componentJsPath = path.join(
|
|
36
|
+
fixturesRoot,
|
|
37
|
+
"components",
|
|
38
|
+
"card",
|
|
39
|
+
"index.js"
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
it("exports a default API object", () => {
|
|
43
|
+
|
|
44
|
+
expect(componentAndPageMemory).toBeTruthy();
|
|
45
|
+
expect(typeof componentAndPageMemory.readFileMemoized).toBe("function");
|
|
46
|
+
expect(typeof componentAndPageMemory.extractUiRefsMemoized).toBe("function");
|
|
47
|
+
expect(typeof componentAndPageMemory.extractUiRefsFromFileMemoized).toBe("function");
|
|
48
|
+
expect(typeof componentAndPageMemory.clearComponentAndPageMemory).toBe("function");
|
|
49
|
+
expect(typeof componentAndPageMemory.getComponentAndPageMemoryStatus).toBe("function");
|
|
50
|
+
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("returns useful status/debug information", () => {
|
|
54
|
+
|
|
55
|
+
clearComponentAndPageMemory();
|
|
56
|
+
const initComponents = extractUiRefsFromFileMemoized(siteInitPath, {
|
|
57
|
+
namespace: "ui",
|
|
58
|
+
includeDotAccess: true,
|
|
59
|
+
});
|
|
60
|
+
const pageJsComponents = extractUiRefsFromFileMemoized(siteJsPath, {
|
|
61
|
+
namespace: "ui",
|
|
62
|
+
includeDotAccess: true,
|
|
63
|
+
});
|
|
64
|
+
const cardComponents = extractUiRefsFromFileMemoized(componentJsPath, {
|
|
65
|
+
namespace: "ui",
|
|
66
|
+
includeDotAccess: true,
|
|
67
|
+
});
|
|
68
|
+
const gatheredComponents = [ ...new Set([
|
|
69
|
+
"card",
|
|
70
|
+
...initComponents,
|
|
71
|
+
...pageJsComponents,
|
|
72
|
+
...cardComponents,
|
|
73
|
+
]) ].sort();
|
|
74
|
+
|
|
75
|
+
expect(initComponents).toEqual(expect.arrayContaining([ "pageInit", "pageWidget" ]));
|
|
76
|
+
expect(pageJsComponents).toEqual(expect.arrayContaining([ "pageScript" ]));
|
|
77
|
+
expect(cardComponents).toEqual(expect.arrayContaining([ "cardMain", "cardMount" ]));
|
|
78
|
+
expect(extractUiRefsMemoized("ui.inlineOnly();", { namespace: "ui" })).toEqual([ "inlineOnly" ]);
|
|
79
|
+
|
|
80
|
+
console.log("components gathered", gatheredComponents);
|
|
81
|
+
expect(gatheredComponents).toEqual([
|
|
82
|
+
"card",
|
|
83
|
+
"cardMain",
|
|
84
|
+
"cardMount",
|
|
85
|
+
"pageInit",
|
|
86
|
+
"pageScript",
|
|
87
|
+
"pageWidget",
|
|
88
|
+
]);
|
|
89
|
+
|
|
90
|
+
const status = getComponentAndPageMemoryStatus({
|
|
91
|
+
includeKeys: true,
|
|
92
|
+
sampleSize: 10,
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
console.log("componentAndPageMemory status", status);
|
|
96
|
+
|
|
97
|
+
expect(status.counts.fileCacheEntries).toBeGreaterThanOrEqual(1);
|
|
98
|
+
expect(status.counts.refsCacheEntries).toBeGreaterThanOrEqual(4);
|
|
99
|
+
expect(status.approximateBytes.totalCacheBytes).toBeGreaterThan(0);
|
|
100
|
+
expect(status.processMemoryUsage).toBeTruthy();
|
|
101
|
+
expect(status.keys.fileCacheKeys).toEqual(expect.arrayContaining([
|
|
102
|
+
siteInitPath,
|
|
103
|
+
siteJsPath,
|
|
104
|
+
componentJsPath,
|
|
105
|
+
]));
|
|
106
|
+
expect(status.keys.refsCacheKeys.length).toBeGreaterThan(0);
|
|
107
|
+
clearComponentAndPageMemory();
|
|
108
|
+
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("shows cold hit vs warm hit timing", () => {
|
|
112
|
+
|
|
113
|
+
const coldRuns = 25;
|
|
114
|
+
const warmRuns = 250;
|
|
115
|
+
|
|
116
|
+
let coldTotalMs = 0;
|
|
117
|
+
|
|
118
|
+
for (let i = 0; i < coldRuns; i++) {
|
|
119
|
+
|
|
120
|
+
clearComponentAndPageMemory();
|
|
121
|
+
|
|
122
|
+
const coldStart = performance.now();
|
|
123
|
+
|
|
124
|
+
extractUiRefsFromFileMemoized(componentJsPath, {
|
|
125
|
+
namespace: "ui",
|
|
126
|
+
includeDotAccess: true,
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
coldTotalMs += performance.now() - coldStart;
|
|
130
|
+
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
clearComponentAndPageMemory();
|
|
134
|
+
extractUiRefsFromFileMemoized(componentJsPath, {
|
|
135
|
+
namespace: "ui",
|
|
136
|
+
includeDotAccess: true,
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
const warmStart = performance.now();
|
|
140
|
+
|
|
141
|
+
for (let i = 0; i < warmRuns; i++) {
|
|
142
|
+
|
|
143
|
+
extractUiRefsFromFileMemoized(componentJsPath, {
|
|
144
|
+
namespace: "ui",
|
|
145
|
+
includeDotAccess: true,
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const warmTotalMs = performance.now() - warmStart;
|
|
151
|
+
const coldAvgMs = coldTotalMs / coldRuns;
|
|
152
|
+
const warmAvgMs = warmTotalMs / warmRuns;
|
|
153
|
+
|
|
154
|
+
console.log("componentAndPageMemory cold vs warm", {
|
|
155
|
+
coldRuns,
|
|
156
|
+
warmRuns,
|
|
157
|
+
coldTotalMs,
|
|
158
|
+
warmTotalMs,
|
|
159
|
+
coldAvgMs,
|
|
160
|
+
warmAvgMs,
|
|
161
|
+
ratioWarmToCold: warmAvgMs / coldAvgMs,
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
// Warm hits should typically be faster or close to cold hits in CI/VM environments.
|
|
165
|
+
expect(warmAvgMs).toBeLessThanOrEqual(coldAvgMs * 1.5);
|
|
166
|
+
|
|
167
|
+
clearComponentAndPageMemory();
|
|
168
|
+
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
});
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { performance } from "perf_hooks";
|
|
4
|
+
import { clearComponentAndPageMemory } from "../src/componentAndPageMemory.js";
|
|
5
|
+
|
|
6
|
+
const mocks = vi.hoisted(() => ({
|
|
7
|
+
componentDependencies: vi.fn(),
|
|
8
|
+
pageInfo: vi.fn(),
|
|
9
|
+
getConfig: vi.fn(),
|
|
10
|
+
}));
|
|
11
|
+
|
|
12
|
+
vi.mock("../src/componentDependencies.js", () => ({
|
|
13
|
+
default: mocks.componentDependencies,
|
|
14
|
+
}));
|
|
15
|
+
|
|
16
|
+
vi.mock("../src/pageInfo.js", () => ({
|
|
17
|
+
default: mocks.pageInfo,
|
|
18
|
+
}));
|
|
19
|
+
|
|
20
|
+
vi.mock("../src/getConfig.js", () => ({
|
|
21
|
+
default: mocks.getConfig,
|
|
22
|
+
}));
|
|
23
|
+
|
|
24
|
+
import fetchPage from "../src/fetchPage.js";
|
|
25
|
+
|
|
26
|
+
describe("fetchPage component inference", () => {
|
|
27
|
+
|
|
28
|
+
const fixturesRoot = path.join(
|
|
29
|
+
process.cwd(),
|
|
30
|
+
"tests",
|
|
31
|
+
"fixtures",
|
|
32
|
+
"site-and-components"
|
|
33
|
+
);
|
|
34
|
+
const jsPath = path.join(
|
|
35
|
+
fixturesRoot,
|
|
36
|
+
"sites",
|
|
37
|
+
"site-a",
|
|
38
|
+
"pages",
|
|
39
|
+
"home",
|
|
40
|
+
"js"
|
|
41
|
+
);
|
|
42
|
+
const cssPath = path.join(
|
|
43
|
+
fixturesRoot,
|
|
44
|
+
"sites",
|
|
45
|
+
"site-a",
|
|
46
|
+
"pages",
|
|
47
|
+
"home",
|
|
48
|
+
"css"
|
|
49
|
+
);
|
|
50
|
+
const expectedComponents = [
|
|
51
|
+
"card",
|
|
52
|
+
"cardMain",
|
|
53
|
+
"cardMount",
|
|
54
|
+
"pageInit",
|
|
55
|
+
"pageScript",
|
|
56
|
+
"pageWidget",
|
|
57
|
+
];
|
|
58
|
+
|
|
59
|
+
function buildRequest() {
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
site: "site-a",
|
|
63
|
+
pageRoute: "home",
|
|
64
|
+
testing: true,
|
|
65
|
+
meta: {
|
|
66
|
+
pages: {},
|
|
67
|
+
components: {},
|
|
68
|
+
},
|
|
69
|
+
updatedMeta: {
|
|
70
|
+
pages: {},
|
|
71
|
+
components: {},
|
|
72
|
+
},
|
|
73
|
+
user: {
|
|
74
|
+
permissions: [ "*" ],
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
beforeEach(() => {
|
|
81
|
+
|
|
82
|
+
vi.clearAllMocks();
|
|
83
|
+
|
|
84
|
+
mocks.getConfig.mockReturnValue({
|
|
85
|
+
siteDir__URL: path.join(fixturesRoot, "sites"),
|
|
86
|
+
componentLocations: () => [ new URL("file:///tmp/components/") ],
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
mocks.pageInfo.mockResolvedValue({
|
|
90
|
+
about: {
|
|
91
|
+
version: "1.0.0",
|
|
92
|
+
permissions: [],
|
|
93
|
+
components: [ "card" ],
|
|
94
|
+
},
|
|
95
|
+
cssPath,
|
|
96
|
+
jsPath,
|
|
97
|
+
getData: async () => ({ ok: true }),
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
mocks.componentDependencies.mockImplementation((components) => {
|
|
101
|
+
|
|
102
|
+
return components.reduce((acc, component) => {
|
|
103
|
+
|
|
104
|
+
acc[component] = {
|
|
105
|
+
css: `/* ${component} css */`,
|
|
106
|
+
js: `/* ${component} js */`,
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
return acc;
|
|
110
|
+
|
|
111
|
+
}, {});
|
|
112
|
+
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("infers components from init.js and page js files", async () => {
|
|
118
|
+
|
|
119
|
+
const awesomenessRequest = {
|
|
120
|
+
site: "site-a",
|
|
121
|
+
pageRoute: "home",
|
|
122
|
+
meta: {
|
|
123
|
+
pages: {},
|
|
124
|
+
components: {},
|
|
125
|
+
},
|
|
126
|
+
updatedMeta: {
|
|
127
|
+
pages: {},
|
|
128
|
+
components: {},
|
|
129
|
+
},
|
|
130
|
+
user: {
|
|
131
|
+
permissions: [ "*" ],
|
|
132
|
+
},
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
const out = await fetchPage(awesomenessRequest, {});
|
|
136
|
+
|
|
137
|
+
expect(out).toEqual({ ok: true });
|
|
138
|
+
expect(mocks.componentDependencies).toHaveBeenCalledTimes(1);
|
|
139
|
+
|
|
140
|
+
const inferred = [ ...new Set(mocks.componentDependencies.mock.calls[0][0]) ].sort();
|
|
141
|
+
|
|
142
|
+
console.log("fetchPage inferred components", inferred);
|
|
143
|
+
|
|
144
|
+
expect(inferred).toEqual(expectedComponents);
|
|
145
|
+
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it("still infers init.js components when page js rebuild is skipped", async () => {
|
|
149
|
+
|
|
150
|
+
const awesomenessRequest = {
|
|
151
|
+
site: "site-a",
|
|
152
|
+
pageRoute: "home",
|
|
153
|
+
meta: {
|
|
154
|
+
pages: {
|
|
155
|
+
home: "1.0.0",
|
|
156
|
+
},
|
|
157
|
+
components: {},
|
|
158
|
+
},
|
|
159
|
+
updatedMeta: {
|
|
160
|
+
pages: {},
|
|
161
|
+
components: {},
|
|
162
|
+
},
|
|
163
|
+
user: {
|
|
164
|
+
permissions: [ "*" ],
|
|
165
|
+
},
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
const out = await fetchPage(awesomenessRequest, {});
|
|
169
|
+
|
|
170
|
+
expect(out).toEqual({ ok: true });
|
|
171
|
+
expect(mocks.componentDependencies).toHaveBeenCalledTimes(1);
|
|
172
|
+
|
|
173
|
+
const inferred = [ ...new Set(mocks.componentDependencies.mock.calls[0][0]) ].sort();
|
|
174
|
+
|
|
175
|
+
console.log("fetchPage inferred components (cached page)", inferred);
|
|
176
|
+
|
|
177
|
+
expect(inferred).toEqual(expectedComponents);
|
|
178
|
+
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it("works when about.components is omitted and still infers from page files", async () => {
|
|
182
|
+
|
|
183
|
+
mocks.pageInfo.mockResolvedValueOnce({
|
|
184
|
+
about: {
|
|
185
|
+
version: "1.0.0",
|
|
186
|
+
permissions: [],
|
|
187
|
+
},
|
|
188
|
+
cssPath,
|
|
189
|
+
jsPath,
|
|
190
|
+
getData: async () => ({ ok: true }),
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
const awesomenessRequest = buildRequest();
|
|
194
|
+
const out = await fetchPage(awesomenessRequest, {});
|
|
195
|
+
|
|
196
|
+
expect(out).toEqual({ ok: true });
|
|
197
|
+
expect(mocks.componentDependencies).toHaveBeenCalledTimes(1);
|
|
198
|
+
|
|
199
|
+
const inferred = [ ...new Set(mocks.componentDependencies.mock.calls[0][0]) ].sort();
|
|
200
|
+
|
|
201
|
+
console.log("fetchPage inferred components (no about.components)", inferred);
|
|
202
|
+
|
|
203
|
+
expect(inferred).toEqual([
|
|
204
|
+
"cardMain",
|
|
205
|
+
"cardMount",
|
|
206
|
+
"pageInit",
|
|
207
|
+
"pageScript",
|
|
208
|
+
"pageWidget",
|
|
209
|
+
]);
|
|
210
|
+
expect(inferred).not.toContain("card");
|
|
211
|
+
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it("shows batch timing with cache retained vs cache cleared", async () => {
|
|
215
|
+
|
|
216
|
+
const requestRuns = 75;
|
|
217
|
+
|
|
218
|
+
clearComponentAndPageMemory();
|
|
219
|
+
mocks.componentDependencies.mockClear();
|
|
220
|
+
|
|
221
|
+
const retainedStart = performance.now();
|
|
222
|
+
|
|
223
|
+
for (let i = 0; i < requestRuns; i++) {
|
|
224
|
+
|
|
225
|
+
await fetchPage(buildRequest(), {});
|
|
226
|
+
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const retainedMs = performance.now() - retainedStart;
|
|
230
|
+
|
|
231
|
+
clearComponentAndPageMemory();
|
|
232
|
+
mocks.componentDependencies.mockClear();
|
|
233
|
+
|
|
234
|
+
const clearedStart = performance.now();
|
|
235
|
+
|
|
236
|
+
for (let i = 0; i < requestRuns; i++) {
|
|
237
|
+
|
|
238
|
+
clearComponentAndPageMemory();
|
|
239
|
+
await fetchPage(buildRequest(), {});
|
|
240
|
+
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const clearedMs = performance.now() - clearedStart;
|
|
244
|
+
const ratioRetainedToCleared = retainedMs / clearedMs;
|
|
245
|
+
|
|
246
|
+
console.log("fetchPage batch cache benchmark", {
|
|
247
|
+
requestRuns,
|
|
248
|
+
retainedMs,
|
|
249
|
+
clearedMs,
|
|
250
|
+
ratioRetainedToCleared,
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
expect(retainedMs).toBeLessThanOrEqual(clearedMs * 1.5);
|
|
254
|
+
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
});
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
export default function card() {
|
|
2
|
+
|
|
3
|
+
const $card = ui.cardMain();
|
|
4
|
+
const mountRef = ui.cardMount.mount();
|
|
5
|
+
|
|
6
|
+
const items = [
|
|
7
|
+
{
|
|
8
|
+
id: "itm-1",
|
|
9
|
+
title: "First",
|
|
10
|
+
score: 11
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
id: "itm-2",
|
|
14
|
+
title: "Second",
|
|
15
|
+
score: 27
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
id: "itm-3",
|
|
19
|
+
title: "Third",
|
|
20
|
+
score: 42
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
id: "itm-4",
|
|
24
|
+
title: "Fourth",
|
|
25
|
+
score: 31
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
id: "itm-5",
|
|
29
|
+
title: "Fifth",
|
|
30
|
+
score: 18
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
id: "itm-6",
|
|
34
|
+
title: "Sixth",
|
|
35
|
+
score: 54
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
id: "itm-7",
|
|
39
|
+
title: "Seventh",
|
|
40
|
+
score: 39
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
id: "itm-8",
|
|
44
|
+
title: "Eighth",
|
|
45
|
+
score: 23
|
|
46
|
+
},
|
|
47
|
+
];
|
|
48
|
+
|
|
49
|
+
const grouped = items.reduce((acc, item) => {
|
|
50
|
+
|
|
51
|
+
const bucket = item.score >= 40 ? "high" : item.score >= 25 ? "medium" : "low";
|
|
52
|
+
|
|
53
|
+
acc[bucket] = acc[bucket] || [];
|
|
54
|
+
acc[bucket].push(item);
|
|
55
|
+
|
|
56
|
+
return acc;
|
|
57
|
+
|
|
58
|
+
}, {});
|
|
59
|
+
|
|
60
|
+
const sorted = [ ...items ].sort((a, b) => b.score - a.score);
|
|
61
|
+
const topThree = sorted.slice(0, 3);
|
|
62
|
+
|
|
63
|
+
const details = {
|
|
64
|
+
count: items.length,
|
|
65
|
+
topThree,
|
|
66
|
+
grouped,
|
|
67
|
+
mountRef,
|
|
68
|
+
signature: `${topThree.map((item) => item.id).join("|")}:${items.length}`,
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
if (details.count > 5) {
|
|
72
|
+
|
|
73
|
+
ui.cardMain();
|
|
74
|
+
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (details.topThree.length === 3) {
|
|
78
|
+
|
|
79
|
+
ui.cardMount.mount();
|
|
80
|
+
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
$card,
|
|
85
|
+
details,
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
export default function() {
|
|
2
|
+
|
|
3
|
+
const items = [
|
|
4
|
+
{
|
|
5
|
+
id: "itm-1",
|
|
6
|
+
title: "First",
|
|
7
|
+
score: 11
|
|
8
|
+
},
|
|
9
|
+
{
|
|
10
|
+
id: "itm-2",
|
|
11
|
+
title: "Second",
|
|
12
|
+
score: 27
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
id: "itm-3",
|
|
16
|
+
title: "Third",
|
|
17
|
+
score: 42
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
id: "itm-4",
|
|
21
|
+
title: "Fourth",
|
|
22
|
+
score: 31
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
id: "itm-5",
|
|
26
|
+
title: "Fifth",
|
|
27
|
+
score: 18
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
id: "itm-6",
|
|
31
|
+
title: "Sixth",
|
|
32
|
+
score: 54
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
id: "itm-7",
|
|
36
|
+
title: "Seventh",
|
|
37
|
+
score: 39
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
id: "itm-8",
|
|
41
|
+
title: "Eighth",
|
|
42
|
+
score: 23
|
|
43
|
+
},
|
|
44
|
+
];
|
|
45
|
+
|
|
46
|
+
const grouped = items.reduce((acc, item) => {
|
|
47
|
+
|
|
48
|
+
const bucket = item.score >= 40 ? "high" : item.score >= 25 ? "medium" : "low";
|
|
49
|
+
|
|
50
|
+
acc[bucket] = acc[bucket] || [];
|
|
51
|
+
acc[bucket].push(item);
|
|
52
|
+
|
|
53
|
+
return acc;
|
|
54
|
+
|
|
55
|
+
}, {});
|
|
56
|
+
|
|
57
|
+
const sorted = [ ...items ].sort((a, b) => b.score - a.score);
|
|
58
|
+
const topThree = sorted.slice(0, 3);
|
|
59
|
+
|
|
60
|
+
const details = {
|
|
61
|
+
count: items.length,
|
|
62
|
+
topThree,
|
|
63
|
+
grouped,
|
|
64
|
+
mountRef,
|
|
65
|
+
signature: `${topThree.map((item) => item.id).join("|")}:${items.length}`,
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
if (details.count > 5) {
|
|
69
|
+
|
|
70
|
+
ui.cardMain();
|
|
71
|
+
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (details.topThree.length === 3) {
|
|
75
|
+
|
|
76
|
+
ui.cardMount.mount();
|
|
77
|
+
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return {
|
|
81
|
+
$card,
|
|
82
|
+
details,
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
export default function() {
|
|
2
|
+
|
|
3
|
+
const items = [
|
|
4
|
+
{
|
|
5
|
+
id: "itm-1",
|
|
6
|
+
title: "First",
|
|
7
|
+
score: 11
|
|
8
|
+
},
|
|
9
|
+
{
|
|
10
|
+
id: "itm-2",
|
|
11
|
+
title: "Second",
|
|
12
|
+
score: 27
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
id: "itm-3",
|
|
16
|
+
title: "Third",
|
|
17
|
+
score: 42
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
id: "itm-4",
|
|
21
|
+
title: "Fourth",
|
|
22
|
+
score: 31
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
id: "itm-5",
|
|
26
|
+
title: "Fifth",
|
|
27
|
+
score: 18
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
id: "itm-6",
|
|
31
|
+
title: "Sixth",
|
|
32
|
+
score: 54
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
id: "itm-7",
|
|
36
|
+
title: "Seventh",
|
|
37
|
+
score: 39
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
id: "itm-8",
|
|
41
|
+
title: "Eighth",
|
|
42
|
+
score: 23
|
|
43
|
+
},
|
|
44
|
+
];
|
|
45
|
+
|
|
46
|
+
const grouped = items.reduce((acc, item) => {
|
|
47
|
+
|
|
48
|
+
const bucket = item.score >= 40 ? "high" : item.score >= 25 ? "medium" : "low";
|
|
49
|
+
|
|
50
|
+
acc[bucket] = acc[bucket] || [];
|
|
51
|
+
acc[bucket].push(item);
|
|
52
|
+
|
|
53
|
+
return acc;
|
|
54
|
+
|
|
55
|
+
}, {});
|
|
56
|
+
|
|
57
|
+
const sorted = [ ...items ].sort((a, b) => b.score - a.score);
|
|
58
|
+
const topThree = sorted.slice(0, 3);
|
|
59
|
+
|
|
60
|
+
const details = {
|
|
61
|
+
count: items.length,
|
|
62
|
+
topThree,
|
|
63
|
+
grouped,
|
|
64
|
+
mountRef,
|
|
65
|
+
signature: `${topThree.map((item) => item.id).join("|")}:${items.length}`,
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
if (details.count > 5) {
|
|
69
|
+
|
|
70
|
+
ui.cardMain();
|
|
71
|
+
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (details.topThree.length === 3) {
|
|
75
|
+
|
|
76
|
+
ui.cardMount.mount();
|
|
77
|
+
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return {
|
|
81
|
+
$card,
|
|
82
|
+
details,
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
}
|