@jsenv/core 40.12.13 → 40.12.14

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.
@@ -1,458 +0,0 @@
1
- import { performance } from "node:perf_hooks";
2
- import { jsenvPluginHtmlSyntaxErrorFallback } from "./html_syntax_error_fallback/jsenv_plugin_html_syntax_error_fallback.js";
3
-
4
- export const createPluginStore = async (plugins) => {
5
- const allDevServerRoutes = [];
6
- const allDevServerServices = [];
7
- const pluginArray = [];
8
-
9
- const pluginPromises = [];
10
- const addPlugin = async (plugin) => {
11
- if (plugin && typeof plugin.then === "function") {
12
- pluginPromises.push(plugin);
13
- const value = await plugin;
14
- addPlugin(value);
15
- return;
16
- }
17
- if (Array.isArray(plugin)) {
18
- for (const subplugin of plugin) {
19
- addPlugin(subplugin);
20
- }
21
- return;
22
- }
23
- if (plugin === null || typeof plugin !== "object") {
24
- throw new TypeError(`plugin must be objects, got ${plugin}`);
25
- }
26
- if (!plugin.name) {
27
- plugin.name = "anonymous";
28
- }
29
- if (plugin.devServerRoutes) {
30
- const devServerRoutes = plugin.devServerRoutes;
31
- for (const devServerRoute of devServerRoutes) {
32
- allDevServerRoutes.push(devServerRoute);
33
- }
34
- }
35
- if (plugin.devServerServices) {
36
- const devServerServices = plugin.devServerServices;
37
- for (const devServerService of devServerServices) {
38
- allDevServerServices.push(devServerService);
39
- }
40
- }
41
- pluginArray.push(plugin);
42
- };
43
- addPlugin(jsenvPluginHtmlSyntaxErrorFallback());
44
- for (const plugin of plugins) {
45
- addPlugin(plugin);
46
- }
47
- await Promise.all(pluginPromises);
48
-
49
- return {
50
- pluginArray,
51
- allDevServerRoutes,
52
- allDevServerServices,
53
- };
54
- };
55
-
56
- export const createPluginController = async (
57
- pluginStore,
58
- kitchen,
59
- { initialPuginsMeta = {} } = {},
60
- ) => {
61
- const pluginsMeta = initialPuginsMeta;
62
- kitchen.context.getPluginMeta = (id) => {
63
- const value = pluginsMeta[id];
64
- return value;
65
- };
66
-
67
- // precompute a list of hooks per hookName because:
68
- // 1. [MAJOR REASON] when debugging, there is less iteration (so much better)
69
- // 2. [MINOR REASON] it should increase perf as there is less work to do
70
- const hookSetMap = new Map();
71
- const pluginCandidates = pluginStore.pluginArray;
72
- const activePluginArray = [];
73
- const pluginWithEffectCandidateForActivationArray = [];
74
- for (const pluginCandidate of pluginCandidates) {
75
- if (!testAppliesDuring(pluginCandidate, kitchen)) {
76
- pluginCandidate.destroy?.();
77
- continue;
78
- }
79
- const initPluginResult = await initPlugin(pluginCandidate, kitchen);
80
- if (!initPluginResult) {
81
- pluginCandidate.destroy?.();
82
- continue;
83
- }
84
- if (pluginCandidate.effect) {
85
- pluginWithEffectCandidateForActivationArray.push(pluginCandidate);
86
- } else {
87
- activePluginArray.push(pluginCandidate);
88
- }
89
- }
90
-
91
- const activeEffectSet = new Set();
92
- for (const pluginWithEffectCandidateForActivation of pluginWithEffectCandidateForActivationArray) {
93
- const returnValue = pluginWithEffectCandidateForActivation.effect({
94
- kitchenContext: kitchen.context,
95
- otherPlugins: activePluginArray,
96
- });
97
- if (!returnValue) {
98
- continue;
99
- }
100
- activePluginArray.push(pluginWithEffectCandidateForActivation);
101
- activeEffectSet.add({
102
- plugin: pluginWithEffectCandidateForActivation,
103
- cleanup: typeof returnValue === "function" ? returnValue : () => {},
104
- });
105
- }
106
- activePluginArray.sort((a, b) => {
107
- return pluginCandidates.indexOf(a) - pluginCandidates.indexOf(b);
108
- });
109
- for (const activePlugin of activePluginArray) {
110
- for (const key of Object.keys(activePlugin)) {
111
- if (key === "meta") {
112
- const value = activePlugin[key];
113
- if (typeof value !== "object" || value === null) {
114
- console.warn(`plugin.meta must be an object, got ${value}`);
115
- continue;
116
- }
117
- Object.assign(pluginsMeta, value);
118
- // any extension/modification on plugin.meta
119
- // won't be taken into account so we freeze object
120
- // to throw in case it happen
121
- Object.freeze(value);
122
- continue;
123
- }
124
- if (
125
- key === "name" ||
126
- key === "appliesDuring" ||
127
- key === "init" ||
128
- key === "serverEvents" ||
129
- key === "mustStayFirst" ||
130
- key === "devServerRoutes" ||
131
- key === "devServerServices" ||
132
- key === "effect"
133
- ) {
134
- continue;
135
- }
136
- const isHook = HOOK_NAMES.includes(key);
137
- if (!isHook) {
138
- console.warn(
139
- `Unexpected "${key}" property on "${activePlugin.name}" plugin`,
140
- );
141
- continue;
142
- }
143
- const hookName = key;
144
- const hookValue = activePlugin[hookName];
145
- if (hookValue) {
146
- let hookSet = hookSetMap.get(hookName);
147
- if (!hookSet) {
148
- hookSet = new Set();
149
- hookSetMap.set(hookName, hookSet);
150
- }
151
- const hook = {
152
- plugin: activePlugin,
153
- name: hookName,
154
- value: hookValue,
155
- };
156
- // if (position === "start") {
157
- // let i = 0;
158
- // while (i < group.length) {
159
- // const before = group[i];
160
- // if (!before.plugin.mustStayFirst) {
161
- // break;
162
- // }
163
- // i++;
164
- // }
165
- // group.splice(i, 0, hook);
166
- // } else {
167
- hookSet.add(hook);
168
- }
169
- }
170
- }
171
-
172
- let lastPluginUsed = null;
173
- let currentPlugin = null;
174
- let currentHookName = null;
175
- const callHook = (hook, info) => {
176
- const hookFn = getHookFunction(hook, info);
177
- if (!hookFn) {
178
- return null;
179
- }
180
- let startTimestamp;
181
- if (info.timing) {
182
- startTimestamp = performance.now();
183
- }
184
- lastPluginUsed = hook.plugin;
185
- currentPlugin = hook.plugin;
186
- currentHookName = hook.name;
187
- let valueReturned = hookFn(info);
188
- if (info.timing) {
189
- info.timing[`${hook.name}-${hook.plugin.name.replace("jsenv:", "")}`] =
190
- performance.now() - startTimestamp;
191
- }
192
- valueReturned = assertAndNormalizeReturnValue(hook, valueReturned, info);
193
- currentPlugin = null;
194
- currentHookName = null;
195
- return valueReturned;
196
- };
197
- const callAsyncHook = async (hook, info) => {
198
- const hookFn = getHookFunction(hook, info);
199
- if (!hookFn) {
200
- return null;
201
- }
202
-
203
- let startTimestamp;
204
- if (info.timing) {
205
- startTimestamp = performance.now();
206
- }
207
- lastPluginUsed = hook.plugin;
208
- currentPlugin = hook.plugin;
209
- currentHookName = hook.name;
210
- let valueReturned = await hookFn(info);
211
- if (info.timing) {
212
- info.timing[`${hook.name}-${hook.plugin.name.replace("jsenv:", "")}`] =
213
- performance.now() - startTimestamp;
214
- }
215
- valueReturned = assertAndNormalizeReturnValue(hook, valueReturned, info);
216
- currentPlugin = null;
217
- currentHookName = null;
218
- return valueReturned;
219
- };
220
- const callHooks = (hookName, info, callback) => {
221
- const hookSet = hookSetMap.get(hookName);
222
- if (!hookSet) {
223
- return;
224
- }
225
- const setHookParams = (firstArg = info) => {
226
- info = firstArg;
227
- };
228
- for (const hook of hookSet) {
229
- const returnValue = callHook(hook, info);
230
- if (returnValue && callback) {
231
- callback(returnValue, hook.plugin, setHookParams);
232
- }
233
- }
234
- };
235
- const callAsyncHooks = async (hookName, info, callback, options) => {
236
- const hookSet = hookSetMap.get(hookName);
237
- if (!hookSet) {
238
- return;
239
- }
240
- for (const hook of hookSet) {
241
- const returnValue = await callAsyncHook(hook, info, options);
242
- if (returnValue && callback) {
243
- await callback(returnValue, hook.plugin);
244
- }
245
- }
246
- };
247
- const callHooksUntil = (hookName, info) => {
248
- const hookSet = hookSetMap.get(hookName);
249
- if (!hookSet) {
250
- return null;
251
- }
252
- for (const hook of hookSet) {
253
- const returnValue = callHook(hook, info);
254
- if (returnValue) {
255
- return returnValue;
256
- }
257
- }
258
- return null;
259
- };
260
- const callAsyncHooksUntil = async (hookName, info, options) => {
261
- const hookSet = hookSetMap.get(hookName);
262
- if (!hookSet) {
263
- return null;
264
- }
265
- if (hookSet.size === 0) {
266
- return null;
267
- }
268
- const iterator = hookSet.values()[Symbol.iterator]();
269
- let result;
270
- const visit = async () => {
271
- const { done, value: hook } = iterator.next();
272
- if (done) {
273
- return;
274
- }
275
- const returnValue = await callAsyncHook(hook, info, options);
276
- if (returnValue) {
277
- result = returnValue;
278
- return;
279
- }
280
- await visit();
281
- };
282
- await visit();
283
- return result;
284
- };
285
-
286
- return {
287
- activePlugins: activePluginArray,
288
-
289
- callHook,
290
- callAsyncHook,
291
- callHooks,
292
- callHooksUntil,
293
- callAsyncHooks,
294
- callAsyncHooksUntil,
295
-
296
- getLastPluginUsed: () => lastPluginUsed,
297
- getCurrentPlugin: () => currentPlugin,
298
- getCurrentHookName: () => currentHookName,
299
- };
300
- };
301
-
302
- const HOOK_NAMES = [
303
- "init",
304
- "devServerRoutes", // is called only during dev/tests
305
- "devServerServices", // is called only during dev/tests
306
- "resolveReference",
307
- "redirectReference",
308
- "transformReferenceSearchParams",
309
- "formatReference",
310
- "urlInfoCreated",
311
- "fetchUrlContent",
312
- "transformUrlContent",
313
- "finalizeUrlContent",
314
- "bundle", // is called only during build
315
- "optimizeBuildUrlContent", // is called only during build
316
- "cooked",
317
- "augmentResponse", // is called only during dev/tests
318
- "destroy",
319
- "effect",
320
- "refineBuildUrlContent", // called only during build
321
- "refineBuild", // called only during build
322
- ];
323
-
324
- const testAppliesDuring = (plugin, kitchen) => {
325
- const { appliesDuring } = plugin;
326
- if (appliesDuring === undefined) {
327
- // console.debug(`"appliesDuring" is undefined on ${pluginEntry.name}`)
328
- return true;
329
- }
330
- if (appliesDuring === "*") {
331
- return true;
332
- }
333
- if (typeof appliesDuring === "string") {
334
- if (appliesDuring !== "dev" && appliesDuring !== "build") {
335
- throw new TypeError(
336
- `"appliesDuring" must be "dev" or "build", got ${appliesDuring}`,
337
- );
338
- }
339
- if (kitchen.context[appliesDuring]) {
340
- return true;
341
- }
342
- return false;
343
- }
344
- if (typeof appliesDuring === "object") {
345
- for (const key of Object.keys(appliesDuring)) {
346
- if (!appliesDuring[key] && kitchen.context[key]) {
347
- return false;
348
- }
349
- if (appliesDuring[key] && kitchen.context[key]) {
350
- return true;
351
- }
352
- }
353
- // throw new Error(`"appliesDuring" is empty`)
354
- return false;
355
- }
356
- throw new TypeError(
357
- `"appliesDuring" must be an object or a string, got ${appliesDuring}`,
358
- );
359
- };
360
- const initPlugin = async (plugin, kitchen) => {
361
- const { init } = plugin;
362
- if (!init) {
363
- return true;
364
- }
365
- const initReturnValue = await init(kitchen.context, { plugin });
366
- if (initReturnValue === false) {
367
- return false;
368
- }
369
- if (typeof initReturnValue === "function" && !plugin.destroy) {
370
- plugin.destroy = initReturnValue;
371
- }
372
- return true;
373
- };
374
- const getHookFunction = (
375
- hook,
376
- // can be undefined, reference, or urlInfo
377
- info = {},
378
- ) => {
379
- const hookValue = hook.value;
380
- if (typeof hookValue === "object") {
381
- const hookForType = hookValue[info.type] || hookValue["*"];
382
- if (!hookForType) {
383
- return null;
384
- }
385
- return hookForType;
386
- }
387
- return hookValue;
388
- };
389
-
390
- const assertAndNormalizeReturnValue = (hook, returnValue, info) => {
391
- // all hooks are allowed to return null/undefined as a signal of "I don't do anything"
392
- if (returnValue === null || returnValue === undefined) {
393
- return returnValue;
394
- }
395
- for (const returnValueAssertion of returnValueAssertions) {
396
- if (!returnValueAssertion.appliesTo.includes(hook.name)) {
397
- continue;
398
- }
399
- const assertionResult = returnValueAssertion.assertion(returnValue, info, {
400
- hook,
401
- });
402
- if (assertionResult !== undefined) {
403
- // normalization
404
- returnValue = assertionResult;
405
- break;
406
- }
407
- }
408
- return returnValue;
409
- };
410
- const returnValueAssertions = [
411
- {
412
- name: "url_assertion",
413
- appliesTo: ["resolveReference", "redirectReference"],
414
- assertion: (valueReturned, urlInfo, { hook }) => {
415
- if (valueReturned instanceof URL) {
416
- return valueReturned.href;
417
- }
418
- if (typeof valueReturned === "string") {
419
- return undefined;
420
- }
421
- throw new Error(
422
- `Unexpected value returned by "${hook.plugin.name}" plugin: it must be a string; got ${valueReturned}`,
423
- );
424
- },
425
- },
426
- {
427
- name: "content_assertion",
428
- appliesTo: [
429
- "fetchUrlContent",
430
- "transformUrlContent",
431
- "finalizeUrlContent",
432
- "optimizeBuildUrlContent",
433
- ],
434
- assertion: (valueReturned, urlInfo, { hook }) => {
435
- if (typeof valueReturned === "string" || Buffer.isBuffer(valueReturned)) {
436
- return { content: valueReturned };
437
- }
438
- if (typeof valueReturned === "object") {
439
- const { content, body } = valueReturned;
440
- if (urlInfo.url.startsWith("ignore:")) {
441
- return undefined;
442
- }
443
- if (typeof content !== "string" && !Buffer.isBuffer(content) && !body) {
444
- if (Object.hasOwn(valueReturned, "contentInjections")) {
445
- return undefined;
446
- }
447
- throw new Error(
448
- `Unexpected "content" returned by "${hook.plugin.name}" ${hook.name} hook: it must be a string or a buffer; got ${content}`,
449
- );
450
- }
451
- return undefined;
452
- }
453
- throw new Error(
454
- `Unexpected value returned by "${hook.plugin.name}" ${hook.name} hook: it must be a string, a buffer or an object; got ${valueReturned}`,
455
- );
456
- },
457
- },
458
- ];