@olenbetong/appframe-vite 4.0.4 → 4.1.1

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.
@@ -3,6 +3,7 @@ export declare function getLoginInfo(): Promise<{
3
3
  hostname: any;
4
4
  username: string;
5
5
  password: string;
6
+ appframe: any;
6
7
  }>;
7
8
  export declare function getProxyRoutes(): string[];
8
9
  export declare function createDevMiddleware(vite: ViteDevServer): Connect.NextHandleFunction;
package/lib/devServer.js CHANGED
@@ -2,8 +2,10 @@ import { Client } from "@olenbetong/appframe-data";
2
2
  import dotenv from "dotenv";
3
3
  import { exists } from "fs-extra";
4
4
  import { JSDOM } from "jsdom";
5
+ import { readFile, stat, writeFile } from "node:fs/promises";
5
6
  import { resolve } from "node:path";
6
7
  import { importJson } from "./importJson.js";
8
+ import { getStringCache } from "./localization.js";
7
9
  dotenv.config();
8
10
  let appPkg = await importJson("./package.json", true);
9
11
  let { appframe } = appPkg;
@@ -14,7 +16,7 @@ export async function getLoginInfo() {
14
16
  if (!username || !password) {
15
17
  throw new Error("Your Appframe credentials are not set in the environment variables. Username should be set in APPFRAME_LOGIN and password in APPFRAME_PWD.");
16
18
  }
17
- return { hostname, username, password };
19
+ return { hostname, username, password, appframe };
18
20
  }
19
21
  export function getProxyRoutes() {
20
22
  const proxyRoutes = [
@@ -40,6 +42,18 @@ export function getProxyRoutes() {
40
42
  }
41
43
  return proxyRoutes;
42
44
  }
45
+ /**
46
+ * We want to keep the development environment as close to the production environment as possible,
47
+ * so we fetch the article HTML from Synergi. There are however some changes we need to do:
48
+ *
49
+ * - Remove any article scripts and styles (they are the production version of the application)
50
+ * - Remove Bugsnag (don't want to report all unhandled exceptions in development)
51
+ * - Remove the web-vitals (only want production results when measuring performance)
52
+ * - Remove the server worker registration
53
+ * - Add strings from local string cache to avoid tons of localize requests when starting the application
54
+ * - Add the hostname to links with absolute paths (they are not part of the application, and will fail without host)
55
+ * @returns
56
+ */
43
57
  async function getArticleHtml() {
44
58
  let { hostname, username, password } = await getLoginInfo();
45
59
  let article = appframe.article?.id;
@@ -78,6 +92,12 @@ async function getArticleHtml() {
78
92
  script.textContent?.includes("serviceWorker.register")) {
79
93
  nodesToRemove.push(script);
80
94
  }
95
+ else if (script.textContent?.includes("af.article.i18n")) {
96
+ let cachedStrings = getStringCache();
97
+ let localizeScript = dom.window.document.createElement("script");
98
+ localizeScript.textContent = `Object.assign(af.article.i18n, ${JSON.stringify(cachedStrings)})`;
99
+ script.insertAdjacentElement("afterend", localizeScript);
100
+ }
81
101
  });
82
102
  dom.window.document.querySelectorAll("link").forEach((link) => {
83
103
  if (link.rel === "stylesheet" && link.href?.startsWith(`/file/article/style/${article}`)) {
@@ -97,15 +117,52 @@ async function getArticleHtml() {
97
117
  }
98
118
  throw Error(`Failed to get article '${article}' from '${hostname}': ${response.status} ${response.statusText}`);
99
119
  }
120
+ const cachePath = resolve(process.cwd(), "./node_modules/.appframe");
121
+ const cacheFile = resolve(cachePath, "./index.html");
122
+ async function getCacheAge() {
123
+ if (await exists(cacheFile)) {
124
+ let file = await stat(cacheFile);
125
+ let age = (Date.now() - file.mtime.getTime()) / 1000;
126
+ return age;
127
+ }
128
+ return -1;
129
+ }
130
+ async function getCachedHtml(age) {
131
+ let maxAge = 3 * 60 * 60; // 3 hours
132
+ if (age < maxAge) {
133
+ return await readFile(cacheFile, { encoding: "utf-8" });
134
+ }
135
+ return "";
136
+ }
100
137
  export function createDevMiddleware(vite) {
101
138
  return async (req, res, next) => {
102
139
  const url = req.originalUrl;
103
- if (url === "/" || url?.startsWith("/" + appPkg.appframe.article.id)) {
140
+ if (url === "/") {
141
+ res.statusCode = 302;
142
+ res.setHeader("Location", `/${appPkg.appframe.article.id}`);
143
+ res.end("");
144
+ }
145
+ else if (url?.startsWith("/" + appPkg.appframe.article.id)) {
104
146
  try {
105
- let html = await getArticleHtml();
106
- html = await vite.transformIndexHtml(url ?? "", html);
147
+ // Unless the browser is explicitly disabling cache by setting the Cache-Control header
148
+ // to "no-cache", we cache the generated HTML for up to 3 hours
149
+ let cacheControl = req.headers["cache-control"];
150
+ let html = "";
151
+ let age = await getCacheAge();
152
+ if (cacheControl !== "no-cache" && age > 0) {
153
+ html = await getCachedHtml(age);
154
+ }
155
+ if (!html) {
156
+ html = await getArticleHtml();
157
+ html = await vite.transformIndexHtml(url ?? "", html);
158
+ await writeFile(cacheFile, html, { encoding: "utf-8" });
159
+ }
107
160
  res.statusCode = 200;
108
161
  res.setHeader("Content-Type", "text/html");
162
+ res.setHeader("Cache-Control", "max-age=10800000");
163
+ if (age >= 0) {
164
+ res.setHeader("Age", age);
165
+ }
109
166
  res.end(html);
110
167
  }
111
168
  catch (error) {
package/lib/index.js CHANGED
@@ -2,6 +2,8 @@ import bodyParser from "body-parser";
2
2
  import { watch } from "chokidar";
3
3
  import { addAppframeBuildConfig } from "./build.js";
4
4
  import { createDevMiddleware, getLoginInfo, getProxyRoutes } from "./devServer.js";
5
+ import { importJson } from "./importJson.js";
6
+ import { localizeMiddleware } from "./localization.js";
5
7
  import { getLastSession, login } from "./proxy.js";
6
8
  let interval;
7
9
  let server;
@@ -101,7 +103,10 @@ export default function appframe() {
101
103
  return config;
102
104
  },
103
105
  async configureServer(_server) {
106
+ let { appframe } = await importJson("./package.json", true);
104
107
  server = _server;
108
+ _server.middlewares.use(`/api/user/localize/new/${appframe.article.id}`, jsonParser);
109
+ _server.middlewares.use(`/api/user/localize/new/${appframe.article.id}`, localizeMiddleware);
105
110
  _server.middlewares.use("/data/Logger/LogError", jsonParser);
106
111
  _server.middlewares.use("/data/Logger/LogError", (req, res) => {
107
112
  // @ts-ignore
@@ -0,0 +1,6 @@
1
+ import { Connect } from "vite";
2
+ export declare function addStringToCache(text: string, translation: string): void;
3
+ export declare const localizeMiddleware: Connect.NextHandleFunction;
4
+ export declare function getStringCache(): {
5
+ [k: string]: string;
6
+ };
@@ -0,0 +1,82 @@
1
+ /**
2
+ * There can be potentially many requests to localize strings on startup,
3
+ * and since the dev server runs on HTTP 1.1, it is limited to just a few
4
+ * requests at the same time. This means starting the application sometimes
5
+ * takes a long time, because the data fetch requests are queued behind a lot
6
+ * of localize requests.
7
+ *
8
+ * To fix this, we add a middleware that handles the localize route, and
9
+ * cache the proxied results in a file under /node_modules/.appframe. In the
10
+ * handler that generates the article HTML, we then get the cached strings,
11
+ * and add a script that adds the strings to the client cache.
12
+ */
13
+ import { exists } from "fs-extra";
14
+ import { mkdir, writeFile } from "node:fs/promises";
15
+ import { resolve } from "node:path";
16
+ import { getLoginInfo } from "./devServer.js";
17
+ import { importJson } from "./importJson.js";
18
+ import { getLastSession } from "./proxy.js";
19
+ function debounce(callback, delay = 300) {
20
+ let timeout;
21
+ return (...args) => {
22
+ clearTimeout(timeout);
23
+ timeout = setTimeout(() => callback(...args), delay);
24
+ };
25
+ }
26
+ const stringCache = new Map();
27
+ const cachePath = resolve(process.cwd(), "./node_modules/.appframe");
28
+ const cacheFile = resolve(cachePath, "./localizeCache.json");
29
+ if (await exists(cacheFile)) {
30
+ let cache = await importJson(cacheFile, true);
31
+ for (let text in cache) {
32
+ stringCache.set(text, cache[text]);
33
+ }
34
+ }
35
+ async function saveCacheToFile() {
36
+ if (!(await exists(cacheFile))) {
37
+ await mkdir(cachePath, { recursive: true });
38
+ }
39
+ let data = JSON.stringify(Object.fromEntries(stringCache));
40
+ await writeFile(cacheFile, data, { encoding: "utf-8" });
41
+ }
42
+ // Avoid running 10s to 100s of writes in a row by debouncing the
43
+ // cache save operation.
44
+ const debouncedSaveCacheToFile = debounce(saveCacheToFile, 1000);
45
+ export function addStringToCache(text, translation) {
46
+ stringCache.set(text, translation);
47
+ debouncedSaveCacheToFile();
48
+ }
49
+ export const localizeMiddleware = async (req, res, next) => {
50
+ // @ts-ignore
51
+ let text = req.body.text;
52
+ let { hostname, appframe } = await getLoginInfo();
53
+ let authCookies = getLastSession(hostname)?.authCookies;
54
+ if (authCookies) {
55
+ let cookies = [];
56
+ cookies.push(`AppframeWebAuth=${authCookies["AppframeWebAuth"]}`);
57
+ cookies.push(`AppframeWebSession=${authCookies["AppframeWebSession"]}`);
58
+ let cookieHeader = cookies.join(";");
59
+ let uri = `https://${hostname}/api/user/localize/new/${appframe.article.id}`;
60
+ let result = await fetch(uri, {
61
+ body: JSON.stringify({ text }),
62
+ method: "POST",
63
+ headers: {
64
+ Accept: "application/json",
65
+ "Content-Type": "application/json",
66
+ Cookie: cookieHeader,
67
+ "X-Requested-With": "XMLHttpRequest",
68
+ },
69
+ });
70
+ let data = await result.json();
71
+ addStringToCache(text, data);
72
+ res.statusCode = 200;
73
+ res.end(JSON.stringify(data));
74
+ }
75
+ else {
76
+ res.statusCode = 200;
77
+ res.end(text);
78
+ }
79
+ };
80
+ export function getStringCache() {
81
+ return Object.fromEntries(stringCache);
82
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@olenbetong/appframe-vite",
3
- "version": "4.0.4",
3
+ "version": "4.1.1",
4
4
  "description": "Tools to use and deploy Vite applications to Appframe",
5
5
  "main": "./lib/index.js",
6
6
  "type": "module",
@@ -43,5 +43,5 @@
43
43
  "open": "^9.1.0",
44
44
  "tcp-port-used": "^1.0.2"
45
45
  },
46
- "gitHead": "dee9e5d023b626a1ad73a94ab880ab875832cdf6"
46
+ "gitHead": "cd010cf9dcd40cda3f30d89fc48d26de17cf2812"
47
47
  }