@venizia/ardor 0.3.6 → 0.3.7
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/dist/presets/app/files/.configs/builders/Dockerfile +8 -2
- package/dist/presets/app/files/.configs/builders/docker-entrypoint.d/40-runtime-config.sh +93 -0
- package/dist/presets/app/files/.configs/builders/nginx.conf.template +6 -0
- package/dist/presets/app/files/_env.example +5 -0
- package/dist/presets/app/files/src/application.test.ts +19 -6
- package/dist/presets/app/files/src/configurations/admin.configuration.ts +11 -2
- package/dist/presets/app/files/src/index.html +3 -0
- package/dist/presets/app/files/src/public/config.js +3 -0
- package/dist/presets/app/preset.json +1 -1
- package/dist/presets/lib/preset.json +2 -2
- package/package.json +4 -4
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
# docker build -f {{appPath}}/.configs/builders/Dockerfile -t {{app}} \
|
|
6
6
|
# --build-arg APP_BUILD_COMMIT="$(git rev-parse HEAD)" \
|
|
7
7
|
# --build-arg APP_BUILD_BRANCH="$(git rev-parse --abbrev-ref HEAD)" .
|
|
8
|
-
# docker run --rm -p 8080:8080 -e API_UPSTREAM=http://api:3000 {{app}}
|
|
8
|
+
# docker run --rm -p 8080:8080 -e API_UPSTREAM=http://api:3000 -e VITE_API_URL=/api {{app}}
|
|
9
9
|
#
|
|
10
10
|
# The preset cannot know which workspace packages this app needs, so the whole context is copied (the
|
|
11
11
|
# .dockerignore at the root filters it), then installed and built. A project can list the packages by
|
|
@@ -64,6 +64,11 @@ LABEL org.opencontainers.image.revision="${APP_BUILD_COMMIT}"
|
|
|
64
64
|
COPY --from=builder /home/bun/app/{{appPath}}/.configs/builders/nginx.conf.template /etc/nginx/templates/default.conf.template
|
|
65
65
|
COPY --from=builder --chown=101:101 /home/bun/app/{{appPath}}/dist /usr/share/nginx/html
|
|
66
66
|
|
|
67
|
+
# The runtime config: this script writes /usr/share/nginx/html/config.js from the VITE_ environment variables
|
|
68
|
+
# when the container starts. 0755 because the base entrypoint skips a script without the executable bit; the
|
|
69
|
+
# file it rewrites is the one the build copied from src/public, owned by 101 above.
|
|
70
|
+
COPY --from=builder --chmod=0755 /home/bun/app/{{appPath}}/.configs/builders/docker-entrypoint.d/40-runtime-config.sh /docker-entrypoint.d/40-runtime-config.sh
|
|
71
|
+
|
|
67
72
|
# Where /api goes: a compose service name, a cluster service, an address (an origin, with no path).
|
|
68
73
|
ENV API_UPSTREAM=http://api:3000
|
|
69
74
|
|
|
@@ -71,7 +76,8 @@ ENV API_UPSTREAM=http://api:3000
|
|
|
71
76
|
# gives to `resolver`: nginx looks the upstream up per request, so the container starts without it.
|
|
72
77
|
ENV NGINX_ENTRYPOINT_LOCAL_RESOLVERS=1
|
|
73
78
|
|
|
74
|
-
# No ENTRYPOINT on purpose: the base image's /docker-entrypoint.sh renders the templates
|
|
79
|
+
# No ENTRYPOINT on purpose: the base image's /docker-entrypoint.sh renders the templates and runs the
|
|
80
|
+
# scripts of /docker-entrypoint.d/ (a custom ENTRYPOINT would replace it and skip both).
|
|
75
81
|
EXPOSE 8080
|
|
76
82
|
|
|
77
83
|
CMD ["nginx", "-g", "daemon off;"]
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
# Writes /usr/share/nginx/html/config.js from the container's environment, so one built image serves
|
|
3
|
+
# every environment: `docker run -e VITE_API_URL=https://api.example.com image`.
|
|
4
|
+
#
|
|
5
|
+
# The nginx image runs every executable file of /docker-entrypoint.d/ in `sort -V` order before it starts
|
|
6
|
+
# nginx. The 40 puts this after the base image's own scripts. A script without the executable bit is
|
|
7
|
+
# skipped, hence `COPY --chmod=0755` in the Dockerfile; the Dockerfile must not declare an ENTRYPOINT,
|
|
8
|
+
# which would replace the script that runs this directory.
|
|
9
|
+
#
|
|
10
|
+
# What is published: the variables named VITE_ followed by capitals, digits and underscores, and nothing
|
|
11
|
+
# else. That is Vite's own rule for what reaches the browser, so the same names work in dev and here.
|
|
12
|
+
# Every published value is readable by every visitor: never put a secret in a VITE_ variable. A name
|
|
13
|
+
# with SECRET, PASSWORD or TOKEN in it stops the container, to catch that mistake early (a name with KEY
|
|
14
|
+
# is fine: public keys are normal). Values are escaped as JavaScript string content, so no value can
|
|
15
|
+
# end its string or the file.
|
|
16
|
+
#
|
|
17
|
+
# RUNTIME_CONFIG_TARGET names another file, for the tests; it is not a VITE_ name, so it is never published.
|
|
18
|
+
set -eu
|
|
19
|
+
|
|
20
|
+
TARGET="${RUNTIME_CONFIG_TARGET:-/usr/share/nginx/html/config.js}"
|
|
21
|
+
|
|
22
|
+
# A read-only file is one the operator mounted over config.js: keep it as it is.
|
|
23
|
+
if [ -e "$TARGET" ] && [ ! -w "$TARGET" ]; then
|
|
24
|
+
echo "[runtime-config] $TARGET is read-only (mounted): keeping the supplied file"
|
|
25
|
+
exit 0
|
|
26
|
+
fi
|
|
27
|
+
|
|
28
|
+
# One awk program, so the values never pass through a shell expansion. ENVIRON holds them as they are.
|
|
29
|
+
BODY=$(awk '
|
|
30
|
+
function escape(text, result, index_, character) {
|
|
31
|
+
result = ""
|
|
32
|
+
for (index_ = 1; index_ <= length(text); index_++) {
|
|
33
|
+
character = substr(text, index_, 1)
|
|
34
|
+
result = result ((character in escapes) ? escapes[character] : character)
|
|
35
|
+
}
|
|
36
|
+
return result
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
BEGIN {
|
|
40
|
+
for (code = 1; code < 32; code++) {
|
|
41
|
+
escapes[sprintf("%c", code)] = sprintf("\\u%04x", code)
|
|
42
|
+
}
|
|
43
|
+
escapes[sprintf("%c", 127)] = "\\u007f"
|
|
44
|
+
escapes["\\"] = "\\\\"
|
|
45
|
+
escapes["\""] = "\\\""
|
|
46
|
+
escapes["<"] = "\\u003c"
|
|
47
|
+
escapes[">"] = "\\u003e"
|
|
48
|
+
escapes["&"] = "\\u0026"
|
|
49
|
+
|
|
50
|
+
count = 0
|
|
51
|
+
failed = 0
|
|
52
|
+
for (name in ENVIRON) {
|
|
53
|
+
if (name !~ /^VITE_[A-Z0-9_]+$/) {
|
|
54
|
+
continue
|
|
55
|
+
}
|
|
56
|
+
if (name ~ /SECRET|PASSWORD|TOKEN/) {
|
|
57
|
+
print "[runtime-config] " name " looks like a secret, and every VITE_ variable is published to every visitor: rename it or keep it out of VITE_" > "/dev/stderr"
|
|
58
|
+
failed = 1
|
|
59
|
+
continue
|
|
60
|
+
}
|
|
61
|
+
names[++count] = name
|
|
62
|
+
}
|
|
63
|
+
if (failed) {
|
|
64
|
+
exit 1
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
for (i = 2; i <= count; i++) {
|
|
68
|
+
current = names[i]
|
|
69
|
+
for (j = i - 1; j >= 1 && names[j] > current; j--) {
|
|
70
|
+
names[j + 1] = names[j]
|
|
71
|
+
}
|
|
72
|
+
names[j + 1] = current
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
for (i = 1; i <= count; i++) {
|
|
76
|
+
printf " \"%s\": \"%s\"%s\n", names[i], escape(ENVIRON[names[i]]), (i < count ? "," : "")
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
' </dev/null) || {
|
|
80
|
+
echo "[runtime-config] FATAL: config.js was not written" >&2
|
|
81
|
+
exit 1
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
{
|
|
85
|
+
printf 'window.__APP_CONFIG__ = Object.freeze({'
|
|
86
|
+
if [ -n "$BODY" ]; then
|
|
87
|
+
printf '\n%s\n' "$BODY"
|
|
88
|
+
fi
|
|
89
|
+
printf '});\n'
|
|
90
|
+
} >"$TARGET"
|
|
91
|
+
|
|
92
|
+
NAMES=$(printf '%s\n' "$BODY" | sed -n 's/^ "\([A-Z0-9_]*\)": .*/\1/p' | tr '\n' ' ')
|
|
93
|
+
echo "[runtime-config] wrote $TARGET with: ${NAMES:-nothing}"
|
|
@@ -20,6 +20,12 @@ server {
|
|
|
20
20
|
proxy_set_header X-Forwarded-Proto $scheme;
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
# The runtime config is written when the container starts and has no hash in its name: never cached.
|
|
24
|
+
location = /config.js {
|
|
25
|
+
add_header Cache-Control "no-store";
|
|
26
|
+
try_files $uri =404;
|
|
27
|
+
}
|
|
28
|
+
|
|
23
29
|
# The built files carry a content hash in their name, so a browser may keep them for good.
|
|
24
30
|
location /assets/ {
|
|
25
31
|
expires 1y;
|
|
@@ -6,3 +6,8 @@ PORT={{devPort}}
|
|
|
6
6
|
|
|
7
7
|
# The backend the dev server forwards /api to. Empty: the one chosen when the app was added (none, if it had no backend).
|
|
8
8
|
API_URL={{apiUrl}}
|
|
9
|
+
|
|
10
|
+
# The API the app calls. Empty: /api. In dev the app reads it as import.meta.env.VITE_API_URL; in the image it
|
|
11
|
+
# is the VITE_API_URL given to `docker run -e`, which config.js carries to the browser when the container starts.
|
|
12
|
+
# Every VITE_ variable is public: anything published is readable by every visitor. Never put a secret in one.
|
|
13
|
+
VITE_API_URL=
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import 'reflect-metadata';
|
|
2
2
|
|
|
3
|
-
import { describe, expect, test } from 'bun:test';
|
|
3
|
+
import { afterEach, describe, expect, test } from 'bun:test';
|
|
4
4
|
|
|
5
5
|
import {
|
|
6
6
|
BaseArdorApplication,
|
|
7
7
|
CoreBindings,
|
|
8
|
+
RUNTIME_CONFIG_GLOBAL,
|
|
8
9
|
type IApplicationInfo,
|
|
9
10
|
type IArtifactIndex,
|
|
10
11
|
} from '@venizia/ardor';
|
|
@@ -24,14 +25,26 @@ class TestApplication extends BaseArdorApplication {
|
|
|
24
25
|
bindContext(): void {}
|
|
25
26
|
}
|
|
26
27
|
|
|
28
|
+
const restUrlOf = async (): Promise<unknown> => {
|
|
29
|
+
const application = new TestApplication();
|
|
30
|
+
|
|
31
|
+
await application.start();
|
|
32
|
+
|
|
33
|
+
return application.get<{ url: string }>({ key: CoreBindings.REST_DATA_PROVIDER_OPTIONS }).url;
|
|
34
|
+
};
|
|
35
|
+
|
|
27
36
|
describe('AdminConfiguration', () => {
|
|
37
|
+
afterEach(() => {
|
|
38
|
+
Reflect.deleteProperty(globalThis, RUNTIME_CONFIG_GLOBAL);
|
|
39
|
+
});
|
|
40
|
+
|
|
28
41
|
test('binds the REST data provider options under their key', async () => {
|
|
29
|
-
|
|
42
|
+
expect(await restUrlOf()).toBe('/api');
|
|
43
|
+
});
|
|
30
44
|
|
|
31
|
-
|
|
45
|
+
test('the API url is the one config.js wrote when the container started', async () => {
|
|
46
|
+
Reflect.set(globalThis, RUNTIME_CONFIG_GLOBAL, { VITE_API_URL: 'https://api.example.com' });
|
|
32
47
|
|
|
33
|
-
expect(
|
|
34
|
-
url: '/api',
|
|
35
|
-
});
|
|
48
|
+
expect(await restUrlOf()).toBe('https://api.example.com');
|
|
36
49
|
});
|
|
37
50
|
});
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
DefaultI18nProvider,
|
|
6
6
|
DefaultRestDataProvider,
|
|
7
7
|
englishMessages,
|
|
8
|
+
getRuntimeConfig,
|
|
8
9
|
inject,
|
|
9
10
|
provide,
|
|
10
11
|
} from '@venizia/ardor';
|
|
@@ -20,8 +21,16 @@ export class AdminConfiguration {
|
|
|
20
21
|
|
|
21
22
|
@provide({ key: CoreBindings.REST_DATA_PROVIDER_OPTIONS })
|
|
22
23
|
restDataProviderOptions() {
|
|
23
|
-
// No sign-in: no request carries an Authorization header.
|
|
24
|
-
|
|
24
|
+
// No sign-in: no request carries an Authorization header. The API is `/api` (the image's nginx
|
|
25
|
+
// forwards it to API_UPSTREAM) unless VITE_API_URL says otherwise: set in .env in dev, or written to
|
|
26
|
+
// config.js when the container starts, so one built image serves every environment.
|
|
27
|
+
const url = getRuntimeConfig({
|
|
28
|
+
key: 'VITE_API_URL',
|
|
29
|
+
buildTimeEnv: import.meta.env,
|
|
30
|
+
defaultValue: '/api',
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
return { url, noAuthPathRegex: /./ };
|
|
25
34
|
}
|
|
26
35
|
|
|
27
36
|
@provide({ key: CoreBindings.I18N_PROVIDER_OPTIONS })
|
|
@@ -9,6 +9,9 @@
|
|
|
9
9
|
</head>
|
|
10
10
|
<body>
|
|
11
11
|
<div id="root"></div>
|
|
12
|
+
<!-- The runtime config: a classic script, so it runs before the module below. The image rewrites it when the
|
|
13
|
+
container starts; src/public/config.js is the empty one that dev and the build use. -->
|
|
14
|
+
<script src="/config.js"></script>
|
|
12
15
|
<script type="module" src="/main.tsx"></script>
|
|
13
16
|
</body>
|
|
14
17
|
</html>
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
"dependencies": {
|
|
14
14
|
"@reduxjs/toolkit": "^2.13.0",
|
|
15
15
|
"@tanstack/react-query": "^5.104.5",
|
|
16
|
-
"@venizia/ardor": "0.3.
|
|
16
|
+
"@venizia/ardor": "0.3.7",
|
|
17
17
|
"@venizia/ignis-connectors": "0.3.6",
|
|
18
18
|
"@venizia/ignis-filter": "0.3.6",
|
|
19
19
|
"@venizia/ignis-helpers": "0.3.6",
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
"devDependencies": {
|
|
6
6
|
"@types/bun": "^1.4.3",
|
|
7
7
|
"@types/react": "^19",
|
|
8
|
-
"@venizia/ardor": "^0.3.
|
|
8
|
+
"@venizia/ardor": "^0.3.7",
|
|
9
9
|
"@venizia/dev-configs": "0.3.6",
|
|
10
10
|
"eslint": "^10.13.0",
|
|
11
11
|
"oxfmt": "0.74.0",
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
"typescript": "^6.0.3"
|
|
19
19
|
},
|
|
20
20
|
"peerDependencies": {
|
|
21
|
-
"@venizia/ardor": "^0.3.
|
|
21
|
+
"@venizia/ardor": "^0.3.7",
|
|
22
22
|
"react": "^19.3.1",
|
|
23
23
|
"react-dom": "^19.3.1",
|
|
24
24
|
"react-router-dom": "^7.18.4",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@venizia/ardor",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.7",
|
|
4
4
|
"description": "ARDOR - frontend application framework for the VENIZIA family. One entry point over ardor-kernel, ardor-react and ardor-admin: IGNIS inversion of control, a REST data layer, auth and i18n providers, and the React bindings for a react-admin application.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ardor",
|
|
@@ -79,9 +79,9 @@
|
|
|
79
79
|
"prepublishOnly": "make rebuild"
|
|
80
80
|
},
|
|
81
81
|
"dependencies": {
|
|
82
|
-
"@venizia/ardor-admin": "0.3.
|
|
83
|
-
"@venizia/ardor-kernel": "0.3.
|
|
84
|
-
"@venizia/ardor-react": "0.3.
|
|
82
|
+
"@venizia/ardor-admin": "0.3.7",
|
|
83
|
+
"@venizia/ardor-kernel": "0.3.7",
|
|
84
|
+
"@venizia/ardor-react": "0.3.7"
|
|
85
85
|
},
|
|
86
86
|
"peerDependencies": {
|
|
87
87
|
"@reduxjs/toolkit": ">=2.13.0",
|