From d6acf825c8529c1e5bccb6c1d4a8952685521485 Mon Sep 17 00:00:00 2001 From: robin <binkrassdufass@gmail.com> Date: Wed, 6 Dec 2023 17:57:25 +0000 Subject: [PATCH] test --- .gitea/workflows/deploy.yaml | 100 +++++++++---------- api/collections/ssr.yml | 2 + api/config.yml | 1 + api/hooks/config-client.js | 3 +- api/hooks/lib/ssr.js | 180 +++++++++++++++++++++++++++++++++++ api/hooks/ssr/get_read.js | 2 +- esbuild.config.js | 34 ++++++- frontend/.htaccess | 7 +- frontend/src/api.ts | 119 +---------------------- frontend/src/ssr.ts | 2 +- types/global.d.ts | 21 ++++ 11 files changed, 299 insertions(+), 172 deletions(-) create mode 100644 api/hooks/lib/ssr.js diff --git a/.gitea/workflows/deploy.yaml b/.gitea/workflows/deploy.yaml index db1a043..a6cecd8 100644 --- a/.gitea/workflows/deploy.yaml +++ b/.gitea/workflows/deploy.yaml @@ -89,11 +89,11 @@ jobs: run: | yarn build - #- name: build ssr - # env: - # FORCE_COLOR: "true" - # run: | - # yarn build:server + - name: build ssr + env: + FORCE_COLOR: "true" + run: | + yarn build:server - name: build legacy env: @@ -101,58 +101,58 @@ jobs: run: | yarn build:legacy - - name: Wait for Live Server - run: | - attempts=0 - max_attempts=2 - while ! curl --output /dev/null --silent --head --fail http://live-server:8081; do - if [ $attempts -eq $max_attempts ]; then - echo "Live server not ready after $max_attempts attempts" - echo "${{ toJson(job) }}" - curl -v http://live-server:8081 - exit 1 - fi - attempts=$((attempts+1)) - echo "Waiting for live-server to be ready... attempt $attempts" - sleep 5 - done + #- name: Wait for Live Server + # run: | + # attempts=0 + # max_attempts=2 + # while ! curl --output /dev/null --silent --head --fail http://live-server:8081; do + # if [ $attempts -eq $max_attempts ]; then + # echo "Live server not ready after $max_attempts attempts" + # echo "${{ toJson(job) }}" + # curl -v http://live-server:8081 + # exit 1 + # fi + # attempts=$((attempts+1)) + # echo "Waiting for live-server to be ready... attempt $attempts" + # sleep 5 + # done - - name: Test HTTP Request - run: | - echo "Live server not ready after $max_attempts attempts" - echo "${{ toJson(job) }}" - echo "${{ job.services.live-server.id }}" - echo "${{ job.services.tibi-server.id }}" - echo "${{ job.services.mongo.id }}" + #- name: Test HTTP Request + # run: | + # echo "Live server not ready after $max_attempts attempts" + # echo "${{ toJson(job) }}" + # echo "${{ job.services.live-server.id }}" + # echo "${{ job.services.tibi-server.id }}" + ## echo "${{ job.services.mongo.id }}" - docker logs "${{ job.services.tibi-server.id }}" - docker logs "${{ job.services.live-server.id }}" - curl -v http://live-server:8081 + # docker logs "${{ job.services.tibi-server.id }}" + # docker logs "${{ job.services.live-server.id }}" + # curl -v http://live-server:8081 - - name: Install Chrome - run: | - wget -q -O - https://dl.google.com/linux/linux_signing_key.pub | sudo apt-key add - - sudo sh -c 'echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google-chrome.list' - - sudo apt-get update - sudo apt-get install -y google-chrome-stable + #- name: Install Chrome + # run: | + # wget -q -O - https://dl.google.com/linux/linux_signing_key.pub | sudo apt-key add - + # sudo sh -c 'echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google-chrome.list' + # + # sudo apt-get update + # sudo apt-get install -y google-chrome-stable # Lighthouse Analysis Step - - name: Lighthouse Analysis - run: | - yarn add lighthouse - npx lighthouse http://127.0.0.1:8081 --output json --output-path /tmp/lighthouse-report.json --chrome-flags="--headless --no-sandbox --disable-dev-shm-usage" + #- name: Lighthouse Analysis + # run: | + # yarn add lighthouse + # npx lighthouse http://127.0.0.1:8081 --output json --output-path /tmp/lighthouse-report.json --chrome-flags="--headless --no-sandbox --disable-dev-shm-usage" # Notify-Lighthouse Step - - name: Notify Lighthouse - run: | - docker run --rm \ - -e PLUGIN_FROM=noreply@gitbase.de \ - -e PLUGIN_HOST=smtp.basehosts.de \ - -e PLUGIN_RECIPIENT=recipient@example.com \ - -e PLUGIN_SUBJECT="Lighthouse Report" \ - -v ${{ github.workspace }}/tmp:/lighthouse-reports \ - drillster/drone-email /tmp/lighthouse-report.json + #- name: Notify Lighthouse + # run: | + # docker run --rm \ + # -e PLUGIN_FROM=noreply@gitbase.de \ + # -e PLUGIN_HOST=smtp.basehosts.de \ + # -e PLUGIN_RECIPIENT=recipient@example.com \ + # -e PLUGIN_SUBJECT="Lighthouse Report" \ + # -v ${{ github.workspace }}/tmp:/lighthouse-reports \ + # drillster/drone-email /tmp/lighthouse-report.json - name: deploy if: github.ref == 'refs/heads/master' diff --git a/api/collections/ssr.yml b/api/collections/ssr.yml index 5dde647..c458313 100644 --- a/api/collections/ssr.yml +++ b/api/collections/ssr.yml @@ -7,6 +7,7 @@ meta: label: { de: "SSR Dummy", en: "ssr dummy" } muiIcon: server rowIdentTpl: { twig: "{{ id }}" } + views: - type: simpleList mediaQuery: "(max-width: 600px)" @@ -55,6 +56,7 @@ fields: - name: path type: string index: [single, unique] + - name: content type: string meta: diff --git a/api/config.yml b/api/config.yml index fe7fffe..ca04f9c 100644 --- a/api/config.yml +++ b/api/config.yml @@ -38,6 +38,7 @@ collections: - !include collections/banner.yml - !include collections/forms.yml - !include collections/backups.yml + - !include collections/ssr.yml jobs: - cron: "0 * * * *" diff --git a/api/hooks/config-client.js b/api/hooks/config-client.js index 0736b02..51b3fd7 100644 --- a/api/hooks/config-client.js +++ b/api/hooks/config-client.js @@ -1,5 +1,5 @@ const release = "tibi-docs.dirty" - +const apiClientBaseURL = "/api/" // @ts-ignore if (release && typeof context !== "undefined") { context.response.header("X-Release", release) @@ -7,4 +7,5 @@ if (release && typeof context !== "undefined") { module.exports = { release, + apiClientBaseURL, } diff --git a/api/hooks/lib/ssr.js b/api/hooks/lib/ssr.js new file mode 100644 index 0000000..e97f4fb --- /dev/null +++ b/api/hooks/lib/ssr.js @@ -0,0 +1,180 @@ +const { apiClientBaseURL } = require("../config-client") + +/** + * convert object to string + * @param {any} obj object + */ +function obj2str(obj) { + if (Array.isArray(obj)) { + return JSON.stringify( + obj.map(function (idx) { + return obj2str(idx) + }) + ) + } else if (typeof obj === "object" && obj !== null) { + var elements = Object.keys(obj) + .sort() + .map(function (key) { + var val = obj2str(obj[key]) + if (val) { + return key + ":" + val + } + }) + + var elementsCleaned = [] + for (var i = 0; i < elements.length; i++) { + if (elements[i]) elementsCleaned.push(elements[i]) + } + + return "{" + elementsCleaned.join("|") + "}" + } + + if (obj) return obj +} + +// fetch polyfill +// [MIT License](LICENSE.md) © [Jason Miller](https://jasonformat.com/) +const _f = function ( + /** @type {string | URL} */ url, + /** @type {{ method?: any; credentials?: any; headers?: any; body?: any; }} */ options +) { + if (typeof XMLHttpRequest === "undefined") { + return Promise.resolve(null) + } + + options = options || {} + return new Promise((resolve, reject) => { + const request = new XMLHttpRequest() + const keys = [] + const all = [] + const headers = {} + + const response = () => ({ + ok: ((request.status / 100) | 0) == 2, // 200-299 + statusText: request.statusText, + status: request.status, + url: request.responseURL, + text: () => Promise.resolve(request.responseText), + json: () => Promise.resolve(request.responseText).then(JSON.parse), + blob: () => Promise.resolve(new Blob([request.response])), + clone: response, + headers: { + // @ts-ignore + keys: () => keys, + // @ts-ignore + entries: () => all, + get: (n) => headers[n.toLowerCase()], + has: (n) => n.toLowerCase() in headers, + }, + }) + + request.open(options.method || "get", url, true) + + request.onload = () => { + request + .getAllResponseHeaders() + // @ts-ignore + .replace(/^(.*?):[^\S\n]*([\s\S]*?)$/gm, (m, key, value) => { + keys.push((key = key.toLowerCase())) + all.push([key, value]) + headers[key] = headers[key] ? `${headers[key]},${value}` : value + }) + resolve(response()) + } + + request.onerror = reject + + request.withCredentials = options.credentials == "include" + + for (const i in options.headers) { + request.setRequestHeader(i, options.headers[i]) + } + + request.send(options.body || null) + }) +} + +const _fetch = typeof fetch === "undefined" ? (typeof window === "undefined" ? _f : window.fetch || _f) : fetch + +/** + * api request via client or server + * server function ssrRequest is called via context.ssrRequest, binded in ssr hook + * + * @param {string} endpoint + * @param {ApiOptions} options + * @param {any} body + * @returns {Promise<ApiResult<any>>} + */ +function apiRequest(endpoint, options, body) { + // TODO cache only for GET + + // first check cache if on client + const cacheKey = obj2str({ endpoint: endpoint, options: options }) + + // @ts-ignore + if (typeof window !== "undefined" && window.__SSR_CACHE__ && options?.method === "GET") { + // @ts-ignore + const cache = window.__SSR_CACHE__[cacheKey] + console.log("SSR:", cacheKey, cache) + if (cache) { + return Promise.resolve(cache) + } + } + + let method = options?.method || "GET" + + let query = "&count=1" + if (options?.filter) query += "&filter=" + encodeURIComponent(JSON.stringify(options.filter)) + if (options?.sort) query += "&sort=" + options.sort + "&sort=_id" + if (options?.limit) query += "&limit=" + options.limit + if (options?.offset) query += "&offset=" + options.offset + if (options?.projection) query += "&projection=" + options.projection + if (options?.lookup) query += "&lookup=" + options.lookup + + if (options?.params) { + Object.keys(options.params).forEach((p) => { + query += "&" + p + "=" + encodeURIComponent(options.params[p]) + }) + } + + let headers = { + "Content-Type": "application/json", + } + + if (options?.headers) headers = { ...headers, ...options.headers } + + if (typeof window === "undefined" && method === "GET") { + // server + + // reference via context from get hook to tree shake in client + // @ts-ignore + const d = context.ssrRequest(cacheKey, endpoint, query, Object.assign({}, options, { method, headers })) + return d + } else { + // client + let url = endpoint + (query ? "?" + query : "") + const requestOptions = { + method, + mode: "cors", + headers, + } + + if (method === "POST" || method === "PUT") { + requestOptions.body = JSON.stringify(body) + } + + return _fetch(apiClientBaseURL + url, requestOptions).then((response) => { + return response?.json().then((json) => { + if (response?.status < 200 || response?.status >= 400) { + return Promise.reject({ response, data: json }) + } + return Promise.resolve({ data: json || null, count: response.headers?.get("x-results-count") || 0 }) + }) + }) + } +} + +module.exports = { + obj2str, + apiRequest, +} diff --git a/api/hooks/ssr/get_read.js b/api/hooks/ssr/get_read.js index 90bff2e..0899f03 100644 --- a/api/hooks/ssr/get_read.js +++ b/api/hooks/ssr/get_read.js @@ -4,7 +4,7 @@ const { obj2str, log } = require("../lib/utils") ;(function () { /** @type {HookResponse} */ var response = null - + console.log("SSR GET READ") var request = context.request() var url = request.query("url") var noCache = request.query("noCache") diff --git a/esbuild.config.js b/esbuild.config.js index c8be6d2..7a474e7 100644 --- a/esbuild.config.js +++ b/esbuild.config.js @@ -1,3 +1,5 @@ +const fs = require("fs") + const resolvePlugin = { name: "resolvePlugin", setup(build) { @@ -68,7 +70,9 @@ const bsMiddleware = [] if (process.argv[2] == "start") { const { createProxyMiddleware } = require("http-proxy-middleware") - const apiBase = process.env.API_BASE || "http://localhost:8080/api/v1/_/" + process.env.NAMESPACE + const dotEnv = fs.readFileSync(__dirname + "/.env", "utf8") + const TIBI_NAMESPACE = dotEnv.match(/TIBI_NAMESPACE=(.*)/)[1] + const apiBase = process.env.API_BASE || "http://localhost:8080/api/v1/_/" + TIBI_NAMESPACE bsMiddleware.push( createProxyMiddleware("/api", { target: apiBase, @@ -77,12 +81,39 @@ if (process.argv[2] == "start") { logLevel: "debug", }) ) + + // if SSR env variable is set + console.log(process.env) + if (process.env.SSR) { + // read api/config.yml.env and read SSR_TOKEN variable from it + const configEnv = fs.readFileSync(__dirname + "/api/config.yml.env", "utf8") + const SSR_TOKEN = configEnv.match(/SSR_TOKEN=(.*)/)[1] + + // redirect all other requests to /api/ssr?token=owshwerNwoa&url=... + bsMiddleware.push( + createProxyMiddleware( + function (path, req) { + return !path.match(/\./) + }, + { + target: apiBase, + changeOrigin: true, + logLevel: "debug", + pathRewrite: function (path, req) { + console.log(path) + return "/ssr?token=" + SSR_TOKEN + "&url=" + encodeURIComponent(path) + }, + } + ) + ) + } } module.exports = { sveltePlugin: sveltePlugin, resolvePlugin: resolvePlugin, options: options, + distDir, watch: { path: [__dirname + "/" + frontendDir + "/src/**/*"], }, @@ -103,7 +134,6 @@ module.exports = { }), ], }, - ghostMode: false, open: false, // logLevel: "debug", }, diff --git a/frontend/.htaccess b/frontend/.htaccess index 5a6b99e..b1409a7 100644 --- a/frontend/.htaccess +++ b/frontend/.htaccess @@ -1,7 +1,7 @@ AddType application/javascript .mjs -DirectoryIndex spa.html -#DirectoryIndex noindex +#DirectoryIndex spa.html +DirectoryIndex noindex <ifModule mod_rewrite.c> RewriteEngine On @@ -11,5 +11,6 @@ DirectoryIndex spa.html RewriteCond %{REQUEST_FILENAME} !-f RewriteCond %{REQUEST_FILENAME} !-d - RewriteRule (.*) /spa.html [QSA,L] + RewriteRule ^/?(.*)$ http://tibi-server:8080/api/v1/_/allkids_erfurt/ssr?token=owshwerNwoa&url=/$1 [P,QSA,L] + #RewriteRule (.*) /spa.html [QSA,L] </ifModule> diff --git a/frontend/src/api.ts b/frontend/src/api.ts index fc5308d..934069f 100644 --- a/frontend/src/api.ts +++ b/frontend/src/api.ts @@ -1,121 +1,12 @@ -import { apiBaseURL } from "./config" - -const _f = function (url, options): Promise<Response> { - if (typeof XMLHttpRequest === "undefined") { - return Promise.resolve(null) - } - - options = options || {} - return new Promise((resolve, reject) => { - const request = new XMLHttpRequest() - const keys = [] - const all = [] - const headers = {} - - const response = (): Response => ({ - ok: ((request.status / 100) | 0) == 2, // 200-299 - statusText: request.statusText, - status: request.status, - url: request.responseURL, - text: () => Promise.resolve(request.responseText), - json: () => Promise.resolve(request.responseText).then(JSON.parse), - blob: () => Promise.resolve(new Blob([request.response])), - clone: response, - headers: { - // @ts-ignore - keys: () => keys, - // @ts-ignore - entries: () => all, - get: (n) => headers[n.toLowerCase()], - has: (n) => n.toLowerCase() in headers, - }, - }) - - request.open(options.method || "get", url, true) - - request.onload = () => { - request - .getAllResponseHeaders() - // @ts-ignore - .replace(/^(.*?):[^\S\n]*([\s\S]*?)$/gm, (m, key, value) => { - keys.push((key = key.toLowerCase())) - all.push([key, value]) - headers[key] = headers[key] ? `${headers[key]},${value}` : value - }) - resolve(response()) - } - - request.onerror = reject - request.withCredentials = options.credentials == "include" - for (const i in options.headers) { - request.setRequestHeader(i, options.headers[i]) - } - request.send(options.body || null) - }) -} - -const _fetch = typeof fetch === "undefined" ? (typeof window === "undefined" ? _f : window.fetch || _f) : fetch +import { apiRequest } from "../../api/hooks/lib/ssr" export const api = async <T>( endpoint: string, - options?: { - method?: string - filter?: any - sort?: string - limit?: number - offset?: number - projection?: string - headers?: { - [key: string]: string - } - params?: { - [key: string]: string - } - }, + options?: ApiOptions, body?: any ): Promise<{ data: T; count: number } | any> => { - if (typeof window === "undefined") { - // ssr - // @ts-ignore - return context.ssrFetch(endpoint, options) - } - - let method = options?.method || "GET" - - let query = "&count=1" - if (options?.filter) query += "&filter=" + encodeURIComponent(JSON.stringify(options.filter)) - if (options?.sort) query += "&sort=" + options.sort + "&sort=_id" - if (options?.limit) query += "&limit=" + options.limit - if (options?.offset) query += "&offset=" + options.offset - if (options?.projection) query += "&projection=" + options.projection - - if (options?.params) { - Object.keys(options.params).forEach((p) => { - query += "&" + p + "=" + encodeURIComponent(options.params[p]) - }) - } - - let headers: any = { - "Content-Type": "application/json", - } - - if (options?.headers) headers = { ...headers, ...options.headers } - - let url = apiBaseURL + endpoint + (query ? "?" + query : "") - - const requestOptions: any = { - method, - mode: "cors", - headers, - } - - if (method === "POST" || method === "PUT") { - requestOptions.body = JSON.stringify(body) - } - - let response = await _fetch(url, requestOptions) - if (response.status == 409 || response.status == 401) return response - let data = (await response?.json()) || null + let data = await apiRequest(endpoint, options, body) // @ts-ignore - return { data } + console.log(data, "data") + return data } diff --git a/frontend/src/ssr.ts b/frontend/src/ssr.ts index a4eea78..1add5ee 100644 --- a/frontend/src/ssr.ts +++ b/frontend/src/ssr.ts @@ -1,3 +1,3 @@ -import App from "./components/App.svelte" +import App from "./App.svelte" export default App diff --git a/types/global.d.ts b/types/global.d.ts index 2ff9c18..7c4c1a1 100644 --- a/types/global.d.ts +++ b/types/global.d.ts @@ -2,6 +2,27 @@ interface Sites { [key: string]: Site } +interface ApiResult<T> { + data: T + count: number +} + +interface ApiOptions { + method?: string + filter?: any + sort?: string + lookup?: string + limit?: number + offset?: number + projection?: string + headers?: { + [key: string]: string + } + params?: { + [key: string]: string + } +} + interface Site { path: string showTeaser: boolean