@awesomeness-js/server 1.1.9 → 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.
Files changed (52) hide show
  1. package/build/build.js +4 -1
  2. package/build/postBuild.js +4 -1
  3. package/index.js +6 -4
  4. package/package.json +1 -1
  5. package/src/componentAndPageMemory.js +269 -0
  6. package/src/componentDependencies.js +9 -19
  7. package/src/extractUiComponentRefs.js +55 -0
  8. package/src/fetchPage.js +103 -4
  9. package/src/resolveRealCasePath.js +0 -4
  10. package/tests/componentAndPageMemory.test.js +171 -0
  11. package/tests/fetchPage.test.js +257 -0
  12. package/tests/fixtures/site-and-components/components/card/index.js +88 -0
  13. package/tests/fixtures/site-and-components/components/cardMain/index.js +86 -0
  14. package/tests/fixtures/site-and-components/components/cardMount/index.js +86 -0
  15. package/tests/fixtures/site-and-components/components/pageInit/index.js +86 -0
  16. package/tests/fixtures/site-and-components/components/pageScript/index.js +86 -0
  17. package/tests/fixtures/site-and-components/components/pageWidget/index.js +86 -0
  18. package/tests/fixtures/site-and-components/sites/site-a/pages/home/init.js +78 -0
  19. package/tests/fixtures/site-and-components/sites/site-a/pages/home/js/index.js +60 -0
  20. package/tests/setup.js +1 -0
  21. package/tsconfig.json +18 -0
  22. package/types/errors.d.ts +0 -0
  23. package/types/index.d.ts +109 -0
  24. package/types/src/applicationMap.d.ts +1 -0
  25. package/types/src/awesomenessNormalizeRequest.d.ts +27 -0
  26. package/types/src/brotliJsonResponse.d.ts +10 -0
  27. package/types/src/checkAccess.d.ts +5 -0
  28. package/types/src/componentAndPageMemory.d.ts +75 -0
  29. package/types/src/componentDependencies.d.ts +6 -0
  30. package/types/src/config.d.ts +7 -0
  31. package/types/src/extractUiComponentRefs.d.ts +5 -0
  32. package/types/src/fetchPage.d.ts +4 -0
  33. package/types/src/getConfig.d.ts +2 -0
  34. package/types/src/getMD.d.ts +1 -0
  35. package/types/src/init.d.ts +2 -0
  36. package/types/src/koa/attachAwesomenessRequest.d.ts +1 -0
  37. package/types/src/koa/cors.d.ts +1 -0
  38. package/types/src/koa/errorHandler.d.ts +1 -0
  39. package/types/src/koa/finalFormat.d.ts +1 -0
  40. package/types/src/koa/jsonBodyParser.d.ts +1 -0
  41. package/types/src/koa/routeRequest.d.ts +1 -0
  42. package/types/src/koa/serverUp.d.ts +1 -0
  43. package/types/src/koa/staticFiles.d.ts +1 -0
  44. package/types/src/koa/timeout.d.ts +1 -0
  45. package/types/src/pageInfo.d.ts +8 -0
  46. package/types/src/reRoute.d.ts +7 -0
  47. package/types/src/resolveRealCasePath.d.ts +1 -0
  48. package/types/src/specialPaths.d.ts +3 -0
  49. package/types/src/start.d.ts +1 -0
  50. package/types/src/validateRequest.d.ts +2 -0
  51. package/types/src/ws/handlers.d.ts +4 -0
  52. package/types/src/ws/index.d.ts +1 -0
package/build/build.js CHANGED
@@ -3,5 +3,8 @@ import { build } from '@awesomeness-js/utils';
3
3
  build({
4
4
  src: './src',
5
5
  dest: './index.js',
6
- dts: false
6
+ dts: false,
7
+ ignore: [
8
+ '*.test.js',
9
+ ]
7
10
  });
@@ -3,5 +3,8 @@ import { build } from '@awesomeness-js/utils';
3
3
  build({
4
4
  src: './src',
5
5
  dest: './types/index.d.ts',
6
- dts: true
6
+ dts: true,
7
+ ignore: [
8
+ '*.test.js',
9
+ ]
7
10
  });
package/index.js CHANGED
@@ -7,8 +7,10 @@ import _applicationMap from './src/applicationMap.js';
7
7
  import _awesomenessNormalizeRequest from './src/awesomenessNormalizeRequest.js';
8
8
  import _brotliJsonResponse from './src/brotliJsonResponse.js';
9
9
  import _checkAccess from './src/checkAccess.js';
10
+ import _componentAndPageMemory from './src/componentAndPageMemory.js';
10
11
  import _componentDependencies from './src/componentDependencies.js';
11
12
  import _config from './src/config.js';
13
+ import _extractUiComponentRefs from './src/extractUiComponentRefs.js';
12
14
  import _fetchPage from './src/fetchPage.js';
13
15
  import _getConfig from './src/getConfig.js';
14
16
  import _getMD from './src/getMD.js';
@@ -35,8 +37,10 @@ export { _applicationMap as applicationMap };
35
37
  export { _awesomenessNormalizeRequest as awesomenessNormalizeRequest };
36
38
  export { _brotliJsonResponse as brotliJsonResponse };
37
39
  export { _checkAccess as checkAccess };
40
+ export { _componentAndPageMemory as componentAndPageMemory };
38
41
  export { _componentDependencies as componentDependencies };
39
42
  export { _config as config };
43
+ export { _extractUiComponentRefs as extractUiComponentRefs };
40
44
  export { _fetchPage as fetchPage };
41
45
  export { _getConfig as getConfig };
42
46
  export { _getMD as getMD };
@@ -71,18 +75,16 @@ export default {
71
75
  awesomenessNormalizeRequest: _awesomenessNormalizeRequest,
72
76
  brotliJsonResponse: _brotliJsonResponse,
73
77
  checkAccess: _checkAccess,
78
+ componentAndPageMemory: _componentAndPageMemory,
74
79
  componentDependencies: _componentDependencies,
75
80
  config: _config,
81
+ extractUiComponentRefs: _extractUiComponentRefs,
76
82
  fetchPage: _fetchPage,
77
83
  getConfig: _getConfig,
78
84
  getMD: _getMD,
79
85
  init: _init,
80
86
  pageInfo: _pageInfo,
81
87
  reRoute: _reRoute,
82
- /**
83
- * Case-insensitive real path resolver.
84
- * Works correctly on Windows, macOS, and Linux/Docker.
85
- */
86
88
  resolveRealCasePath: _resolveRealCasePath,
87
89
  specialPaths: _specialPaths,
88
90
  start: _start,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@awesomeness-js/server",
3
- "version": "1.1.9",
3
+ "version": "1.1.11",
4
4
  "description": "Awesomeness Multi-Site Server",
5
5
  "author": "Scott Forte",
6
6
  "type": "module",
@@ -0,0 +1,269 @@
1
+ import { createHash } from "crypto";
2
+ import { readFileSync, statSync } from "fs";
3
+ import extractUiComponentRefs from "./extractUiComponentRefs.js";
4
+
5
+ const FILE_CACHE_LIMIT = 2000;
6
+ const REFS_CACHE_LIMIT = 5000;
7
+
8
+ const fileCache = new Map();
9
+ const refsCache = new Map();
10
+
11
+ function toMB(bytes) {
12
+
13
+ return Number((bytes / (1024 * 1024)).toFixed(6));
14
+
15
+ }
16
+
17
+ function toGB(bytes) {
18
+
19
+ return Number((bytes / (1024 * 1024 * 1024)).toFixed(6));
20
+
21
+ }
22
+
23
+ function withMBGB(bytes) {
24
+
25
+ return {
26
+ bytes,
27
+ mb: toMB(bytes),
28
+ gb: toGB(bytes),
29
+ };
30
+
31
+ }
32
+
33
+ function pruneCache(cache, limit) {
34
+
35
+ if (cache.size <= limit) {
36
+
37
+ return;
38
+
39
+ }
40
+
41
+ const deleteCount = cache.size - limit;
42
+ let i = 0;
43
+
44
+ for (const key of cache.keys()) {
45
+
46
+ cache.delete(key);
47
+ i += 1;
48
+
49
+ if (i >= deleteCount) {
50
+
51
+ break;
52
+
53
+ }
54
+
55
+ }
56
+
57
+ }
58
+
59
+ function hashContent(content) {
60
+
61
+ return createHash("sha1").update(content).digest("hex");
62
+
63
+ }
64
+
65
+ function refsCacheKey(content, options) {
66
+
67
+ const {
68
+ namespace = "ui",
69
+ includeCall = true,
70
+ includeDotAccess = false,
71
+ cacheContext = "",
72
+ } = options || {};
73
+ const contentHash = hashContent(content);
74
+ const contextPart = cacheContext ? `${cacheContext}|` : "";
75
+
76
+ return `${contextPart}${namespace}|${includeCall}|${includeDotAccess}|${contentHash}`;
77
+
78
+ }
79
+
80
+ function refsCacheKeyFromFile(filePath, fileMeta, options = {}) {
81
+
82
+ const {
83
+ namespace = "ui",
84
+ includeCall = true,
85
+ includeDotAccess = false,
86
+ cacheContext = `file:${filePath}`,
87
+ } = options || {};
88
+ const contextPart = cacheContext ? `${cacheContext}|` : "";
89
+
90
+ return `${contextPart}${namespace}|${includeCall}|${includeDotAccess}|mtime:${fileMeta.mtimeMs}|size:${fileMeta.size}`;
91
+
92
+ }
93
+
94
+ function getFileCacheEntry(filePath) {
95
+
96
+ const stat = statSync(filePath);
97
+ const cacheEntry = fileCache.get(filePath);
98
+
99
+ if (
100
+ cacheEntry &&
101
+ cacheEntry.mtimeMs === stat.mtimeMs &&
102
+ cacheEntry.size === stat.size
103
+ ) {
104
+
105
+ return cacheEntry;
106
+
107
+ }
108
+
109
+ const content = readFileSync(filePath, "utf-8");
110
+ const nextEntry = {
111
+ mtimeMs: stat.mtimeMs,
112
+ size: stat.size,
113
+ content,
114
+ };
115
+
116
+ fileCache.set(filePath, nextEntry);
117
+ pruneCache(fileCache, FILE_CACHE_LIMIT);
118
+
119
+ return nextEntry;
120
+
121
+ }
122
+
123
+ export function readFileMemoized(filePath) {
124
+
125
+ return getFileCacheEntry(filePath).content;
126
+
127
+ }
128
+
129
+ export function extractUiRefsMemoized(content, options = {}) {
130
+
131
+ if (typeof content !== "string" || !content.length) {
132
+
133
+ return [];
134
+
135
+ }
136
+
137
+ const key = refsCacheKey(content, options);
138
+ const existing = refsCache.get(key);
139
+
140
+ if (existing) {
141
+
142
+ return existing;
143
+
144
+ }
145
+
146
+ const refs = extractUiComponentRefs(content, options);
147
+
148
+ refsCache.set(key, refs);
149
+ pruneCache(refsCache, REFS_CACHE_LIMIT);
150
+
151
+ return refs;
152
+
153
+ }
154
+
155
+ export function extractUiRefsFromFileMemoized(filePath, options = {}) {
156
+
157
+ const fileEntry = getFileCacheEntry(filePath);
158
+ const extractionOptions = options.cacheContext
159
+ ? options
160
+ : {
161
+ ...options,
162
+ cacheContext: `file:${filePath}`,
163
+ };
164
+ const key = refsCacheKeyFromFile(filePath, fileEntry, extractionOptions);
165
+ const existing = refsCache.get(key);
166
+
167
+ if (existing) {
168
+
169
+ return existing;
170
+
171
+ }
172
+
173
+ const refs = extractUiComponentRefs(fileEntry.content, extractionOptions);
174
+
175
+ refsCache.set(key, refs);
176
+ pruneCache(refsCache, REFS_CACHE_LIMIT);
177
+
178
+ return refs;
179
+
180
+ }
181
+
182
+ export function clearComponentAndPageMemory() {
183
+
184
+ fileCache.clear();
185
+ refsCache.clear();
186
+
187
+ }
188
+
189
+ export function getComponentAndPageMemoryStatus({
190
+ includeKeys = true,
191
+ sampleSize = 25,
192
+ } = {}) {
193
+
194
+ let fileCacheBytes = 0;
195
+
196
+ for (const value of fileCache.values()) {
197
+
198
+ fileCacheBytes += Buffer.byteLength(value.content || "", "utf-8");
199
+
200
+ }
201
+
202
+ let refsCacheBytes = 0;
203
+
204
+ for (const [ key, value ] of refsCache.entries()) {
205
+
206
+ refsCacheBytes += Buffer.byteLength(key, "utf-8");
207
+ refsCacheBytes += Buffer.byteLength(JSON.stringify(value || []), "utf-8");
208
+
209
+ }
210
+
211
+ const processMemoryUsage = typeof process?.memoryUsage === "function"
212
+ ? process.memoryUsage()
213
+ : null;
214
+ const totalCacheBytes = fileCacheBytes + refsCacheBytes;
215
+ const processMemoryUsageWithUnits = processMemoryUsage
216
+ ? {
217
+ rss: withMBGB(processMemoryUsage.rss || 0),
218
+ heapTotal: withMBGB(processMemoryUsage.heapTotal || 0),
219
+ heapUsed: withMBGB(processMemoryUsage.heapUsed || 0),
220
+ external: withMBGB(processMemoryUsage.external || 0),
221
+ arrayBuffers: withMBGB(processMemoryUsage.arrayBuffers || 0),
222
+ }
223
+ : null;
224
+
225
+ const out = {
226
+ limits: {
227
+ fileCacheLimit: FILE_CACHE_LIMIT,
228
+ refsCacheLimit: REFS_CACHE_LIMIT,
229
+ },
230
+ counts: {
231
+ fileCacheEntries: fileCache.size,
232
+ refsCacheEntries: refsCache.size,
233
+ },
234
+ approximateBytes: {
235
+ fileCacheBytes,
236
+ refsCacheBytes,
237
+ totalCacheBytes,
238
+ },
239
+ approximateMemory: {
240
+ fileCache: withMBGB(fileCacheBytes),
241
+ refsCache: withMBGB(refsCacheBytes),
242
+ totalCache: withMBGB(totalCacheBytes),
243
+ },
244
+ processMemoryUsage,
245
+ processMemoryUsageWithUnits,
246
+ };
247
+
248
+ if (includeKeys) {
249
+
250
+ out.keys = {
251
+ fileCacheKeys: [ ...fileCache.keys() ].slice(0, sampleSize),
252
+ refsCacheKeys: [ ...refsCache.keys() ].slice(0, sampleSize),
253
+ };
254
+
255
+ }
256
+
257
+ return out;
258
+
259
+ }
260
+
261
+ const componentAndPageMemory = {
262
+ readFileMemoized,
263
+ extractUiRefsMemoized,
264
+ extractUiRefsFromFileMemoized,
265
+ clearComponentAndPageMemory,
266
+ getComponentAndPageMemoryStatus,
267
+ };
268
+
269
+ export default componentAndPageMemory;
@@ -1,8 +1,11 @@
1
1
  import path from "path";
2
2
  import { fileURLToPath } from "url";
3
3
  import { each, getAllFiles } from "@awesomeness-js/utils";
4
- import { readFileSync } from "fs";
5
4
  import getConfig from "./getConfig.js";
5
+ import {
6
+ extractUiRefsFromFileMemoized,
7
+ readFileMemoized,
8
+ } from "./componentAndPageMemory.js";
6
9
 
7
10
  function urlToFsPath(u) {
8
11
 
@@ -34,22 +37,6 @@ function urlToFsPath(u) {
34
37
 
35
38
  }
36
39
 
37
- function extractUiFirstParts(str) {
38
-
39
- const regex = /ui\.([a-zA-Z0-9_]+)(?:\.[a-zA-Z0-9_.]*)?\(/g;
40
- const matches = new Set();
41
- let match;
42
-
43
- while ((match = regex.exec(str)) !== null) {
44
-
45
- matches.add(match[1]);
46
-
47
- }
48
-
49
- return [ ...matches ];
50
-
51
- }
52
-
53
40
  export default function componentDependencies(
54
41
  allComponents,
55
42
  {
@@ -186,14 +173,17 @@ export default function componentDependencies(
186
173
 
187
174
  // readFileSync must use chosenRoot + relative file path
188
175
  const filePath = path.isAbsolute(file) ? file : path.join(chosenRoot, file);
189
- const fileContent = readFileSync(filePath, "utf-8");
176
+ const fileContent = readFileMemoized(filePath);
190
177
 
191
178
  const lines = fileContent.split("\n");
192
179
  let fileWithImportsStripped = "";
193
180
 
194
181
  try {
195
182
 
196
- const newTest = extractUiFirstParts(fileContent);
183
+ const newTest = extractUiRefsFromFileMemoized(filePath, {
184
+ namespace,
185
+ cacheContext: `component:${component}|file:${filePath}`,
186
+ });
197
187
 
198
188
  if (newTest.length > 0) {
199
189
 
@@ -0,0 +1,55 @@
1
+ function escapeRegex(input) {
2
+
3
+ return String(input).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
4
+
5
+ }
6
+
7
+ export default function extractUiComponentRefs(
8
+ str,
9
+ {
10
+ namespace = "ui",
11
+ includeCall = true,
12
+ includeDotAccess = false,
13
+ } = {}
14
+ ) {
15
+
16
+ if (typeof str !== "string" || !str.length) {
17
+
18
+ return [];
19
+
20
+ }
21
+
22
+ const ns = escapeRegex(namespace);
23
+ const lookaheads = [];
24
+
25
+ if (includeDotAccess) {
26
+
27
+ lookaheads.push("\\.");
28
+
29
+ }
30
+
31
+ if (includeCall) {
32
+
33
+ lookaheads.push("\\(");
34
+
35
+ }
36
+
37
+ if (!lookaheads.length) {
38
+
39
+ return [];
40
+
41
+ }
42
+
43
+ const regex = new RegExp(`${ns}\\.([a-zA-Z0-9_]+)(?=${lookaheads.join("|")})`, "g");
44
+ const matches = new Set();
45
+ let match;
46
+
47
+ while ((match = regex.exec(str)) !== null) {
48
+
49
+ matches.add(match[1]);
50
+
51
+ }
52
+
53
+ return [ ...matches ];
54
+
55
+ }
package/src/fetchPage.js CHANGED
@@ -1,10 +1,12 @@
1
1
  import path from "path";
2
2
  import { fileURLToPath } from "url";
3
+ import { existsSync } from "fs";
3
4
 
4
5
  import componentDependencies from "./componentDependencies.js";
5
6
  import pageInfo from "./pageInfo.js";
6
7
  import { each, md5, combineFiles } from "@awesomeness-js/utils";
7
8
  import getConfig from "./getConfig.js";
9
+ import { extractUiRefsFromFileMemoized, extractUiRefsMemoized } from "./componentAndPageMemory.js";
8
10
 
9
11
  const componentNamespace = "ui";
10
12
  const pageNamespaceBase = `app.pages`;
@@ -51,6 +53,80 @@ export default async function fetchPage(
51
53
  return pageFnName;
52
54
 
53
55
  }
56
+
57
+ function pageNamespaceInit(pageFnName) {
58
+
59
+ const parts = pageFnName.split(".");
60
+ const inits = [];
61
+
62
+ for (let i = 2; i < parts.length - 1; i++) {
63
+
64
+ inits.push(
65
+ `${parts.slice(0, i + 1).join(".")} = ${parts.slice(0, i + 1).join(".")} || {};`
66
+ );
67
+
68
+ }
69
+
70
+ return inits.join("\n");
71
+
72
+ }
73
+
74
+ function collectComponentsFromPageFiles() {
75
+
76
+ const found = new Set();
77
+ const initPath = path.join(path.dirname(jsPath), "init.js");
78
+ const cacheScope = `site:${awesomenessRequest.site}|page:${page}`;
79
+
80
+ if (existsSync(initPath)) {
81
+
82
+ try {
83
+
84
+
85
+ extractUiRefsFromFileMemoized(initPath, {
86
+ namespace: componentNamespace,
87
+ includeDotAccess: true,
88
+ }).forEach((name) => found.add(name));
89
+
90
+ } catch (err) {
91
+
92
+ console.log("failed to infer components from page init.js", {
93
+ page,
94
+ initPath,
95
+ err,
96
+ });
97
+
98
+ }
99
+
100
+ }
101
+
102
+ try {
103
+
104
+ combineFiles(jsPath, "js", {
105
+ processContent: ({ content }) => {
106
+
107
+ extractUiRefsMemoized(content, {
108
+ namespace: componentNamespace,
109
+ includeDotAccess: true,
110
+ cacheContext: `${cacheScope}|js-content`,
111
+ }).forEach((name) => found.add(name));
112
+
113
+ return content;
114
+
115
+ },
116
+ });
117
+
118
+ } catch (err) {
119
+
120
+ console.log("failed to infer components from page js", {
121
+ page,
122
+ err,
123
+ });
124
+
125
+ }
126
+
127
+ return [ ...found ];
128
+
129
+ }
54
130
 
55
131
 
56
132
  // initialize if not already available
@@ -156,6 +232,7 @@ export default async function fetchPage(
156
232
  }) => {
157
233
 
158
234
  const fnName = pageFn(path, awesomenessRequest);
235
+ const namespaceInit = pageNamespaceInit(fnName);
159
236
 
160
237
  content = content.replaceAll(`import ui from '#ui';`, "");
161
238
  content = content.replaceAll(`import ui from "#ui";`, "");
@@ -165,7 +242,14 @@ export default async function fetchPage(
165
242
  content = content.replaceAll("export default async", `${fnName} = async`);
166
243
  content = content.replaceAll("export default", `${fnName} =`);
167
244
 
168
- return content;
245
+ if (namespaceInit) {
246
+
247
+ content = `${namespaceInit}\n${content}`;
248
+
249
+ }
250
+
251
+ return content;
252
+
169
253
 
170
254
  },
171
255
  });
@@ -194,9 +278,24 @@ export default async function fetchPage(
194
278
 
195
279
  }
196
280
 
197
- if (about?.components?.length) {
198
-
199
- const allDependencies = componentDependencies(about.components, {
281
+ const explicitComponents = Array.isArray(about?.components) ? about.components : [];
282
+ const inferredComponents = [
283
+ ...new Set([
284
+ ...collectComponentsFromPageFiles(),
285
+ ...(meta.pages[page]?.js
286
+ ? extractUiRefsMemoized(meta.pages[page].js, {
287
+ namespace: componentNamespace,
288
+ includeDotAccess: true,
289
+ cacheContext: `site:${awesomenessRequest.site}|page:${page}|bundled-js`,
290
+ })
291
+ : []),
292
+ ]),
293
+ ];
294
+ const pageComponents = [ ...new Set([ ...explicitComponents, ...inferredComponents ]) ];
295
+
296
+ if (pageComponents.length) {
297
+
298
+ const allDependencies = componentDependencies(pageComponents, {
200
299
  componentLocations: awesomenessConfig.componentLocations(awesomenessRequest),
201
300
  namespace: componentNamespace,
202
301
  showDetails,
@@ -1,10 +1,6 @@
1
1
  import { readdirSync } from "fs";
2
2
  import path from "path";
3
3
 
4
- /**
5
- * Case-insensitive real path resolver.
6
- * Works correctly on Windows, macOS, and Linux/Docker.
7
- */
8
4
  export default function resolveRealCasePath(inputPath, returnAbsolute = false) {
9
5
 
10
6
  if (!inputPath) return null;