@olenbetong/appframe-vite 3.1.17 → 4.0.0-rc.0

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/lib/build.js CHANGED
@@ -1,3 +1,5 @@
1
+ import { exists } from "fs-extra";
2
+ import { resolve } from "node:path";
1
3
  import { visualizer } from "rollup-plugin-visualizer";
2
4
  import { viteExternalsPlugin } from "vite-plugin-externals";
3
5
  import { importJson } from "./importJson.js";
@@ -17,7 +19,15 @@ export async function addAppframeBuildConfig(config) {
17
19
  "react-dom": "ReactDOM",
18
20
  }));
19
21
  }
22
+ let entry = resolve(process.cwd(), "./src/index.tsx");
23
+ if (!(await exists(resolve(entry)))) {
24
+ entry = resolve(process.cwd(), "./src/index.ts");
25
+ }
26
+ if (!(await exists(resolve(entry)))) {
27
+ entry = resolve(process.cwd(), "./src/index.js");
28
+ }
20
29
  config.build.rollupOptions = {
30
+ input: entry,
21
31
  plugins: [visualizer({ filename: "./dist/stats.html", gzipSize: true })],
22
32
  output: {
23
33
  globals: appframe.build.externals !== false
@@ -1,6 +1,8 @@
1
- import { Application, RequestHandler, Router } from "express";
2
- import { ViteDevServer } from "vite";
3
- export declare function createProxyMiddleware(): Router;
4
- export declare function createHtmlTransformer(): (html: string) => Promise<string>;
5
- export declare function createDevMiddleware(vite: ViteDevServer): RequestHandler;
6
- export declare function startDevServer(app: Application): Promise<void>;
1
+ import { Connect, ViteDevServer } from "vite";
2
+ export declare function getLoginInfo(): {
3
+ hostname: any;
4
+ username: string;
5
+ password: string;
6
+ };
7
+ export declare function getProxyRoutes(): string[];
8
+ export declare function createDevMiddleware(vite: ViteDevServer): Connect.NextHandleFunction;
package/lib/devServer.js CHANGED
@@ -1,31 +1,13 @@
1
1
  import { Client } from "@olenbetong/appframe-data";
2
- import { watch } from "chokidar";
3
2
  import dotenv from "dotenv";
4
- import express from "express";
5
- import fs from "node:fs/promises";
6
- import path from "node:path";
7
- import open from "open";
8
- import tcpPortUsed from "tcp-port-used";
3
+ import { exists } from "fs-extra";
4
+ import { JSDOM } from "jsdom";
5
+ import { resolve } from "node:path";
9
6
  import { importJson } from "./importJson.js";
10
- import { createAppframeProxy } from "./proxy.js";
11
7
  dotenv.config();
12
8
  let appPkg = await importJson("./package.json", true);
13
9
  let { appframe } = appPkg;
14
- let userSession = null;
15
- async function getUserSession() {
16
- if (userSession) {
17
- return userSession;
18
- }
19
- let { hostname, username, password } = await getLoginInfo();
20
- let client = new Client(hostname);
21
- await client.login(username, password);
22
- let userResult = await client.afFetch("/api/system/usersession", {
23
- timeout: 5000,
24
- });
25
- userSession = await userResult.json();
26
- return userSession;
27
- }
28
- function getLoginInfo() {
10
+ export function getLoginInfo() {
29
11
  const hostname = appframe.proxy?.hostname ?? appframe.deploy?.hostname ?? "dev.obet.no";
30
12
  const { APPFRAME_LOGIN: username, APPFRAME_PWD: password } = process.env;
31
13
  if (!username || !password) {
@@ -33,173 +15,100 @@ function getLoginInfo() {
33
15
  }
34
16
  return { hostname, username, password };
35
17
  }
36
- export function createProxyMiddleware() {
37
- let { hostname, username, password } = getLoginInfo();
38
- const proxy = createAppframeProxy({ hostname, username, password });
39
- try {
40
- let appPkgUrl = `file://${process.cwd()}/package.json`;
41
- watch(appPkgUrl).on("all", async () => {
42
- appPkg = await importJson("./package.json", true);
43
- appframe = appPkg.appframe;
44
- let { hostname } = getLoginInfo();
45
- proxy.setHostname(hostname);
46
- });
47
- }
48
- catch (error) {
49
- console.log(`Failed to watch package.json: ${error.message}`);
50
- }
18
+ export function getProxyRoutes() {
51
19
  const proxyRoutes = [
52
- "/api/*",
53
- "/file/*",
54
- "/filestore/*",
55
- "/static/dbimages*",
56
- "/static/images*",
57
- "/destroy/*",
58
- "/create/*",
59
- "/update/*",
60
- "/retrieve/*",
61
- "/exec/*",
62
- "/report/*",
63
- "/rowcount/*",
64
- "/lib/*",
20
+ "^/api/.*",
21
+ "^/file/.*",
22
+ "^/filestore/.*",
23
+ "^/static/dbimages/.*",
24
+ "^/static/images/.*",
25
+ "^/static/graphics/.*",
26
+ "^/destroy/.*",
27
+ "^/create/.*",
28
+ "^/update/.*",
29
+ "^/retrieve/.*",
30
+ "^/exec/.*",
31
+ "^/report/.*",
32
+ "^/rowcount/.*",
33
+ "^/lib/.*",
34
+ "^/login",
35
+ "^/logout",
65
36
  ];
66
37
  if (appframe.proxy?.routes) {
67
38
  proxyRoutes.push(...appframe.proxy.routes);
68
39
  }
69
- const router = express.Router();
70
- proxyRoutes.forEach((route) => {
71
- router.use(route, proxy);
72
- });
73
- return router;
40
+ return proxyRoutes;
74
41
  }
75
- export function createHtmlTransformer() {
76
- return async (html) => {
77
- let isPartner = appPkg.appframe?.article?.hostname === "partner.olenbetong.no";
78
- let userSession = await getUserSession();
79
- return html
80
- .replace("<!-- {Appframe} -->", `<script src="/lib/@olenbetong/appframe-core/2.7.3/dist/iife/af.common.js"></script>
81
- <script src="/lib/@olenbetong/appframe-data/0.8.4/dist/iife/af.data.js"></script>
82
- <template id="PromptBeforeReloadDialog">
83
- <dialog class="do-prompt">
84
- <form method="dialog">
85
- <article>
86
- <p>There are unsaved changes. Would you like to save before reloading data?
87
- </article>
88
- <footer>
89
- <menu>
90
- <li><button value="cancel">Cancel reloading</button></li>
91
- <li><button value="reload">Reload without saving</button></li>
92
- <li><button class="primary" value="save-and-reload">Save and reload</button></li>
93
- </menu>
94
- </footer>
95
- </form>
96
- </dialog>
97
- </template>
98
-
99
- <script type="text/javascript">
100
- function waitForDialog(dialog) {
101
- return new Promise((resolve) => {
102
- dialog.addEventListener("close", resolve, { once: true });
42
+ async function getArticleHtml() {
43
+ let { hostname, username, password } = getLoginInfo();
44
+ let article = appframe.article?.id;
45
+ let client = new Client(hostname);
46
+ await client.login(username, password);
47
+ let response = await client.afFetch(`/${article}`, {
48
+ timeout: 30000,
103
49
  });
104
- }
105
- async function promptBeforeDataObjectReload() {
106
- let template = document.querySelector("#PromptBeforeReloadDialog");
107
- let dialog = template.content.firstElementChild.cloneNode(true);
108
- document.body.appendChild(dialog);
109
- dialog.showModal();
110
-
111
- await waitForDialog(dialog);
112
- let result = dialog.returnValue || "cancel";
113
- document.body.removeChild(dialog);
114
-
115
- if (result === "save-and-reload") {
116
- await this.endEdit();
117
- }
118
-
119
- return result !== "cancel";
120
- }
121
-
122
- af.DataObject.prototype.beforeReload = promptBeforeDataObjectReload;
123
- </script>
124
- <script>
125
- af = globalThis.af ?? {};
126
- af.userSession = ${JSON.stringify(userSession)};
127
- </script>
128
- <script src="/file/article/static-script/${appframe.article.id}.js"></script>
129
- <script>
130
- var l = window.af?.common?.getLocalizedString ?? ((s) => s);
131
- var SkipToConfig = {
132
- 'settings': {
133
- 'skipTo': {
134
- landmarks: 'main, [role="main"], [role="search"], nav, [role="navigation"], aside, [role="complementary"], section',
135
- headings: 'main h1, main h2, main h3',
136
- colorTheme: 'default',
137
- displayOption: "popup",
138
- positionLeft: "0",
139
- buttonLabel: l("Skip to Content"),
140
- buttonTooltipAccesskey: l('Accesskey is "$key"'),
141
- menuLabel: l('Landmarks and Headings'),
142
- landmarkGroupLabel: l('Landmarks'),
143
- headingGroupLabel: l('Headings'),
144
- mofnGroupLabel: l('$m of $n'),
145
- headingLevelLabel: l('Heading level'),
146
- mainLabel: l('main'),
147
- searchLabel: l('search'),
148
- navLabel: l('navigation'),
149
- asideLabel: l('aside'),
150
- footerLabel: l('footer'),
151
- headerLabel: l('header'),
152
- formLabel: l('form'),
153
- msgNoLandmarksFound: l('No landmarks to skip to'),
154
- msgNoHeadingsFound: l('No main headings to skip to')
50
+ if (response.ok) {
51
+ let html = await response.text();
52
+ let entry;
53
+ if (await exists(resolve(process.cwd(), "./src/index.tsx"))) {
54
+ entry = "/src/index.tsx";
55
+ }
56
+ else if (await exists(resolve(process.cwd(), "./src/index.ts"))) {
57
+ entry = "/src/index.ts";
58
+ }
59
+ else if (await exists(resolve(process.cwd(), "./src/index.js"))) {
60
+ entry = "/src/index.js";
61
+ }
62
+ let dom = new JSDOM(html);
63
+ let nodesToRemove = [];
64
+ dom.window.document.querySelectorAll("script").forEach((script) => {
65
+ if (script.src?.startsWith(`/file/article/script/${article}/main`)) {
66
+ let mainScript = dom.window.document.createElement("script");
67
+ mainScript.type = "module";
68
+ mainScript.src = entry;
69
+ script.insertAdjacentElement("beforebegin", mainScript);
70
+ nodesToRemove.push(script);
71
+ }
72
+ else if (script.src?.includes("bugsnag")) {
73
+ nodesToRemove.push(script);
74
+ }
75
+ else if (script.textContent?.includes("bugsnagOptions") ||
76
+ script.textContent?.includes("web-vitals") ||
77
+ script.textContent?.includes("serviceWorker.register")) {
78
+ nodesToRemove.push(script);
79
+ }
80
+ });
81
+ dom.window.document.querySelectorAll("link").forEach((link) => {
82
+ if (link.rel === "stylesheet" && link.href?.startsWith(`/file/article/style/${article}`)) {
83
+ nodesToRemove.push(link);
84
+ }
85
+ else if (link.rel === "prefetch" && link.href?.startsWith(`/file/article`)) {
86
+ nodesToRemove.push(link);
87
+ }
88
+ });
89
+ nodesToRemove.forEach((node) => node.remove());
90
+ return dom.serialize();
155
91
  }
156
- }
157
- };
158
- </script>
159
- <script src="/lib/paypal-skipto/downloads/js/skipto.min.js" async></script>`)
160
- .replace(`<!-- {Theme} -->`, isPartner
161
- ? `
162
- <link rel="stylesheet" href="/file/site/style/ob.application.min.css" />
163
- <link rel="stylesheet" href="/file/site/style/ob.theme.base.less" />
164
- <link rel="stylesheet" href="/file/site/style/ob.theme.betongost.less" />
165
- <link rel="stylesheet" href="/file/site/style/ob.theme.olenbetong.less" />
166
- <link rel="stylesheet" href="/file/site/style/ob.theme.ribe.less" />
167
- <link rel="stylesheet" href="/file/site/style/ob.theme.sjb.less" />
168
- `
169
- : `
170
- <link rel="stylesheet" href="/file/site/style/ob.theme.base.less" />
171
- <link rel="stylesheet" href="/file/site/style/ob.theme.betongost.less" />
172
- <link rel="stylesheet" href="/file/site/style/ob.theme.forsand.less" />
173
- <link rel="stylesheet" href="/file/site/style/ob.theme.olenbetong.less" />
174
- <link rel="stylesheet" href="/file/site/style/ob.theme.ribe.less" />
175
- <link rel="stylesheet" href="/file/site/style/ob.theme.sjb.less" />
176
- <link rel="stylesheet" href="/file/site/style/ob.es.application.min.css" />`);
177
- };
92
+ throw Error(`Failed to get article '${article}' from '${hostname}': ${response.status} ${response.statusText}`);
178
93
  }
179
94
  export function createDevMiddleware(vite) {
180
95
  return async (req, res, next) => {
181
- let transformer = createHtmlTransformer();
182
96
  const url = req.originalUrl;
183
- try {
184
- let template = await fs.readFile(path.resolve(process.cwd(), "index.html"), "utf-8");
185
- template = await vite.transformIndexHtml(url, template);
186
- let html = await transformer(template);
187
- res.status(200).set({ "Content-Type": "text/html" }).end(html);
97
+ if (url === "/" || url?.startsWith("/" + appPkg.appframe.article.id)) {
98
+ try {
99
+ let html = await getArticleHtml();
100
+ html = await vite.transformIndexHtml(url ?? "", html);
101
+ res.statusCode = 200;
102
+ res.setHeader("Content-Type", "text/html");
103
+ res.end(html);
104
+ }
105
+ catch (error) {
106
+ vite.ssrFixStacktrace(error);
107
+ next(error);
108
+ }
188
109
  }
189
- catch (error) {
190
- vite.ssrFixStacktrace(error);
191
- next(error);
110
+ else {
111
+ next();
192
112
  }
193
113
  };
194
114
  }
195
- export async function startDevServer(app) {
196
- let port = 3000;
197
- let used = true;
198
- do {
199
- used = await tcpPortUsed.check(port);
200
- if (used)
201
- port++;
202
- } while (used);
203
- app.listen(port);
204
- open(`http://localhost:${port}/${appframe.article.id}`);
205
- }
package/lib/index.d.ts CHANGED
@@ -1,6 +1,5 @@
1
1
  import { Plugin } from "vite";
2
2
  import { addAppframeBuildConfig } from "./build.js";
3
- import { createDevMiddleware, createProxyMiddleware, startDevServer } from "./devServer.js";
3
+ import { createDevMiddleware } from "./devServer.js";
4
4
  export default function appframe(): Plugin;
5
- export { addAppframeBuildConfig };
6
- export { createDevMiddleware, createProxyMiddleware, startDevServer };
5
+ export { addAppframeBuildConfig, createDevMiddleware };
package/lib/index.js CHANGED
@@ -1,15 +1,24 @@
1
1
  import { addAppframeBuildConfig } from "./build.js";
2
- import { createDevMiddleware, createHtmlTransformer, createProxyMiddleware, startDevServer } from "./devServer.js";
2
+ import { createDevMiddleware, getLoginInfo, getProxyRoutes } from "./devServer.js";
3
+ import { getLastSession, login } from "./proxy.js";
3
4
  export default function appframe() {
4
- let transformIndexHtml = createHtmlTransformer();
5
5
  return {
6
6
  name: "appframe",
7
+ resolveId(source) {
8
+ if (source.startsWith("/file/") || source.startsWith("/lib/")) {
9
+ return false;
10
+ }
11
+ return null;
12
+ },
7
13
  async config(currentConfig, env) {
8
14
  const { command } = env;
9
15
  let config = {
16
+ ...currentConfig,
10
17
  build: {
18
+ ...currentConfig.build,
11
19
  target: currentConfig.build?.target ?? ["chrome90", "safari14"],
12
20
  rollupOptions: {
21
+ ...currentConfig.build?.rollupOptions,
13
22
  output: {
14
23
  assetFileNames: (assetInfo) => {
15
24
  if (assetInfo.name === "style.css")
@@ -20,23 +29,55 @@ export default function appframe() {
20
29
  },
21
30
  },
22
31
  resolve: {
32
+ ...currentConfig.resolve,
23
33
  alias: [{ find: "~/", replacement: "/src/" }],
24
34
  },
25
35
  };
26
36
  if (command === "build") {
27
37
  await addAppframeBuildConfig(config);
28
38
  }
39
+ let proxy = config.server?.proxy ?? {};
40
+ let routes = getProxyRoutes();
41
+ let { hostname, username, password } = getLoginInfo();
42
+ await login(hostname, username, password);
43
+ // After changing the plugin to use the Vite proxy instead of a custom
44
+ // express server with node-http-proxy, we can no longer ensure the requests
45
+ // have a valid login. To reduce the amount of problems we get with expired
46
+ // sessions, we run the login procedure every 5 minutes to renew the session.
47
+ setInterval(async () => {
48
+ await login(hostname, username, password);
49
+ }, 1000 * 60 * 5);
50
+ for (let route of routes) {
51
+ proxy[route] = {
52
+ target: `https://${hostname}`,
53
+ changeOrigin: true,
54
+ configure(proxy, options) {
55
+ proxy.on("proxyReq", (proxyReq, req, res, options) => {
56
+ let authCookies = getLastSession(hostname)?.authCookies;
57
+ if (authCookies) {
58
+ let cookies = [];
59
+ cookies.push(`AppframeWebAuth=${authCookies["AppframeWebAuth"]}`);
60
+ cookies.push(`AppframeWebSession=${authCookies["AppframeWebSession"]}`);
61
+ proxyReq.setHeader("cookie", cookies.join(";"));
62
+ // If left unchanged, the origin will be the local IP address, which makes Appframe
63
+ // return 403 errors
64
+ proxyReq.setHeader("origin", `https://${hostname}`);
65
+ }
66
+ });
67
+ },
68
+ };
69
+ }
70
+ config.server = {
71
+ ...config.server,
72
+ proxy,
73
+ };
29
74
  return config;
30
75
  },
31
76
  async configureServer(_server) {
32
- let proxyMiddleware = createProxyMiddleware();
33
- _server.middlewares.use(proxyMiddleware);
34
77
  return () => {
35
78
  _server.middlewares.use(createDevMiddleware(_server));
36
79
  };
37
80
  },
38
- transformIndexHtml,
39
81
  };
40
82
  }
41
- export { addAppframeBuildConfig };
42
- export { createDevMiddleware, createProxyMiddleware, startDevServer };
83
+ export { addAppframeBuildConfig, createDevMiddleware };
package/lib/proxy.d.ts CHANGED
@@ -1,5 +1,10 @@
1
1
  import e from "express";
2
2
  type CookieObject = Record<string, string>;
3
+ type LoginCacheEntry = {
4
+ timestamp: number;
5
+ authCookies: CookieObject;
6
+ };
7
+ export declare function getLastSession(hostname: any): LoginCacheEntry | undefined;
3
8
  export declare function login(hostname: string, username: string, password: string): Promise<CookieObject>;
4
9
  export type createProxyOptions = {
5
10
  /**
package/lib/proxy.js CHANGED
@@ -2,6 +2,9 @@ import expressProxy from "express-http-proxy";
2
2
  import https from "node:https";
3
3
  const lastLogin = new Map();
4
4
  let currentLogin = null;
5
+ export function getLastSession(hostname) {
6
+ return lastLogin.get(hostname);
7
+ }
5
8
  export async function login(hostname, username, password) {
6
9
  if (currentLogin) {
7
10
  await currentLogin;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@olenbetong/appframe-vite",
3
- "version": "3.1.17",
3
+ "version": "4.0.0-rc.0",
4
4
  "description": "Tools to use and deploy Vite applications to Appframe",
5
5
  "main": "./lib/index.js",
6
6
  "type": "module",
@@ -22,10 +22,12 @@
22
22
  "@olenbetong/appframe-data": "^0.8.10",
23
23
  "chokidar": "^3.5.3",
24
24
  "dotenv": "^16.3.1",
25
+ "jsdom": "^22.1.0",
25
26
  "rollup-plugin-visualizer": "^5.9.2",
26
27
  "vite-plugin-externals": "^0.6.2"
27
28
  },
28
29
  "devDependencies": {
30
+ "@types/jsdom": "^21.1.2",
29
31
  "typescript": "^5.1.6"
30
32
  },
31
33
  "peerDependencies": {
@@ -40,5 +42,5 @@
40
42
  "open": "^9.1.0",
41
43
  "tcp-port-used": "^1.0.2"
42
44
  },
43
- "gitHead": "cfb5910e5d7e2159b65a467ee8ba31b31c74370e"
45
+ "gitHead": "0875957c231dcd1c5b3e765f45d5a3017c1e30b3"
44
46
  }