@abgov/nx-adsp 13.1.2 → 13.1.4
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/README.md +3 -4
- package/package.json +1 -1
- package/src/generators/vue-app/files/AGENTS.md__tmpl__ +37 -15
- package/src/generators/vue-app/files/public/nginx.conf__tmpl__ +0 -4
- package/src/generators/vue-app/files/src/App.spec.ts__tmpl__ +24 -13
- package/src/generators/vue-app/files/src/App.vue__tmpl__ +13 -9
- package/src/generators/vue-app/files/src/main.ts__tmpl__ +15 -2
- package/src/generators/vue-app/files/src/router/index.ts__tmpl__ +6 -4
- package/src/generators/vue-app/files/src/views/HomeView.vue__tmpl__ +17 -7
- package/src/generators/vue-app/files/src/views/ProtectedView.vue__tmpl__ +7 -3
- package/src/generators/vue-app/vue-app.js +4 -3
- package/src/generators/vue-app/vue-app.js.map +1 -1
- package/src/generators/vue-app/vue-app.spec.ts +43 -2
- package/src/utils/quality.js +5 -1
- package/src/utils/quality.js.map +1 -1
- package/src/utils/quality.spec.ts +67 -0
- package/src/generators/vue-app/files/public/silent-check-sso.html__tmpl__ +0 -5
package/README.md
CHANGED
|
@@ -44,9 +44,8 @@ npx nx g @abgov/nx-adsp:express-service my-service --env dev --tenant my-tenant
|
|
|
44
44
|
## Full-stack quickstart (PEVN → sandbox)
|
|
45
45
|
|
|
46
46
|
This is the end-to-end path a coding agent can follow from an empty folder to a
|
|
47
|
-
running full-stack app in an OpenShift sandbox.
|
|
48
|
-
|
|
49
|
-
`latest`.
|
|
47
|
+
running full-stack app in an OpenShift sandbox. The plugins require **Nx 23**,
|
|
48
|
+
which `create-nx-workspace@latest` produces by default.
|
|
50
49
|
|
|
51
50
|
Prerequisites for the sandbox deploy (steps 4–5): `podman` (machine started on
|
|
52
51
|
macOS), `oc` logged in to the sandbox cluster, and the GitHub CLI (`gh`)
|
|
@@ -66,7 +65,7 @@ cd my-solution
|
|
|
66
65
|
|
|
67
66
|
# 2. Install the plugins + stack peers (match @nx/* to the workspace nx version)
|
|
68
67
|
NXV=$(node -p "require('./node_modules/nx/package.json').version")
|
|
69
|
-
npm i -D @abgov/nx-oc
|
|
68
|
+
npm i -D @abgov/nx-oc @abgov/nx-adsp "@nx/express@$NXV" "@nx/vue@$NXV" "@nx/node@$NXV" "@nx/js@$NXV" "@nx/eslint@$NXV"
|
|
70
69
|
|
|
71
70
|
# 3. Scaffold a Postgres + Express + Vue + Node solution (opens a browser for the ADSP tenant login)
|
|
72
71
|
npx nx g @abgov/nx-adsp:pevn acme --env=dev --tenant=my-tenant --no-interactive
|
package/package.json
CHANGED
|
@@ -40,24 +40,47 @@ executors read options from `project.json` and do not prompt.
|
|
|
40
40
|
|
|
41
41
|
## Auth pattern
|
|
42
42
|
|
|
43
|
+
> ⚠️ **Never destructure `useKeycloak()`.** Unlike most Vue composables (which
|
|
44
|
+
> return `ref`s that survive destructuring), this wrapper returns a
|
|
45
|
+
> `readonly(reactive({...}))` whose fields are **plain values** — `authenticated`
|
|
46
|
+
> is a `boolean`, `keycloak` is the instance-or-`undefined`, etc. Every field is
|
|
47
|
+
> `false`/`undefined` at setup time and only populates **asynchronously** once
|
|
48
|
+
> Keycloak's init settles. `const { keycloak } = useKeycloak()` therefore captures
|
|
49
|
+
> a permanent `undefined`, so `keycloak?.login()` becomes a silent no-op and
|
|
50
|
+
> `authenticated` never flips — and it *looks* fine because the initial render is
|
|
51
|
+
> correct. Always keep the object (`const kc = useKeycloak()`) and read `kc.field`
|
|
52
|
+
> at call/render time. This is the single most common way to break auth here.
|
|
53
|
+
|
|
43
54
|
```typescript
|
|
44
55
|
import { useKeycloak } from '@dsb-norge/vue-keycloak-js';
|
|
45
56
|
|
|
46
|
-
// useKeycloak() returns a
|
|
47
|
-
|
|
57
|
+
// useKeycloak() returns a readonly(reactive(...)) instance. Keep the object and
|
|
58
|
+
// read fields off it — do NOT destructure. Destructuring snapshots each field at
|
|
59
|
+
// setup time, so `kc.keycloak` would freeze at `undefined` (login() a no-op) and
|
|
60
|
+
// `kc.authenticated` would never flip to true. All fields populate asynchronously
|
|
61
|
+
// once Keycloak's init settles.
|
|
62
|
+
const kc = useKeycloak();
|
|
48
63
|
|
|
49
|
-
authenticated
|
|
50
|
-
fullName
|
|
51
|
-
ready
|
|
52
|
-
token
|
|
64
|
+
kc.authenticated // boolean — true after sign-in
|
|
65
|
+
kc.fullName // string | undefined — display name from token
|
|
66
|
+
kc.ready // boolean — true once Keycloak init has settled
|
|
67
|
+
kc.token // string | undefined — current access token
|
|
53
68
|
|
|
54
69
|
// Call Keycloak actions via the underlying keycloak-js instance:
|
|
55
|
-
keycloak?.login()
|
|
56
|
-
keycloak?.logout({ redirectUri: window.location.origin })
|
|
70
|
+
kc.keycloak?.login()
|
|
71
|
+
kc.keycloak?.logout({ redirectUri: window.location.origin })
|
|
57
72
|
|
|
58
73
|
// Refresh token before an authenticated API call:
|
|
59
|
-
await keycloak?.updateToken(30);
|
|
60
|
-
const bearer = keycloak?.token;
|
|
74
|
+
await kc.keycloak?.updateToken(30);
|
|
75
|
+
const bearer = kc.keycloak?.token;
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
To run work when auth settles (it flips after mount, e.g. on return from the
|
|
79
|
+
login redirect), `watch` it rather than reading once:
|
|
80
|
+
|
|
81
|
+
```typescript
|
|
82
|
+
import { watch } from 'vue';
|
|
83
|
+
watch(() => kc.authenticated, (authed) => { if (authed) loadPrivateData(); }, { immediate: true });
|
|
61
84
|
```
|
|
62
85
|
|
|
63
86
|
Route guard (in `router/index.ts`):
|
|
@@ -65,10 +88,10 @@ Route guard (in `router/index.ts`):
|
|
|
65
88
|
```typescript
|
|
66
89
|
router.beforeEach((to) => {
|
|
67
90
|
if (to.meta.requiresAuth) {
|
|
68
|
-
const
|
|
69
|
-
if (!ready) return true; // let Keycloak init settle first
|
|
70
|
-
if (!authenticated) {
|
|
71
|
-
keycloak?.login({ redirectUri: window.location.origin + to.fullPath });
|
|
91
|
+
const kc = useKeycloak();
|
|
92
|
+
if (!kc.ready) return true; // let Keycloak init settle first
|
|
93
|
+
if (!kc.authenticated) {
|
|
94
|
+
kc.keycloak?.login({ redirectUri: window.location.origin + to.fullPath });
|
|
72
95
|
return false;
|
|
73
96
|
}
|
|
74
97
|
}
|
|
@@ -155,7 +178,6 @@ nx run <%= projectName %>:teardown-dev # remove from dev environment
|
|
|
155
178
|
|
|
156
179
|
- `src/main.ts` — Keycloak plugin is registered once here; do not call `new Keycloak()` elsewhere
|
|
157
180
|
- `environments/environment.ts` — access URL and realm are pre-configured for the ADSP tenant
|
|
158
|
-
- `silent-check-sso.html` — required for keycloak-js silent SSO; must be served from the app root
|
|
159
181
|
- `vite.config.ts` — the `isCustomElement` predicate must stay to suppress Vue warnings for `goa-*` elements
|
|
160
182
|
|
|
161
183
|
## Sandbox deployment (local build)
|
|
@@ -1,27 +1,38 @@
|
|
|
1
1
|
import { mount } from '@vue/test-utils';
|
|
2
|
-
import { ref } from 'vue';
|
|
3
2
|
import { describe, it, expect, vi } from 'vitest';
|
|
4
3
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
4
|
+
// Mirror the real wrapper: useKeycloak() returns a reactive() whose fields are
|
|
5
|
+
// plain values (NOT refs), with login/logout on the nested `keycloak` instance.
|
|
6
|
+
// Build the state inside the (hoisted) factory to dodge vi.mock hoisting issues.
|
|
7
|
+
vi.mock('@dsb-norge/vue-keycloak-js', async () => {
|
|
8
|
+
const { reactive } = await import('vue');
|
|
9
|
+
const kc = reactive({
|
|
10
|
+
authenticated: false,
|
|
11
|
+
fullName: '',
|
|
12
|
+
ready: true,
|
|
11
13
|
keycloak: { login: vi.fn(), logout: vi.fn() },
|
|
12
|
-
})
|
|
13
|
-
}
|
|
14
|
+
});
|
|
15
|
+
return { useKeycloak: () => kc };
|
|
16
|
+
});
|
|
14
17
|
|
|
18
|
+
import { useKeycloak } from '@dsb-norge/vue-keycloak-js';
|
|
15
19
|
import App from './App.vue';
|
|
16
20
|
|
|
17
21
|
describe('App shell header', () => {
|
|
18
|
-
it('shows Sign in when unauthenticated
|
|
22
|
+
it('shows Sign in when unauthenticated', () => {
|
|
19
23
|
const wrapper = mount(App, { global: { stubs: { RouterView: true } } });
|
|
20
|
-
|
|
21
|
-
// The sign-in affordance must not be gated on `ready`: if check-sso never
|
|
22
|
-
// resolves, the button would be hidden and the user could never log in.
|
|
23
24
|
const group = wrapper.find('goa-button-group');
|
|
24
25
|
expect(group.exists()).toBe(true);
|
|
25
26
|
expect(group.html()).toContain('Sign in');
|
|
26
27
|
});
|
|
28
|
+
|
|
29
|
+
it('Sign in click calls keycloak.login (guards against lost reactivity)', async () => {
|
|
30
|
+
// The bug: destructuring useKeycloak() froze `keycloak` at undefined, so
|
|
31
|
+
// login() was a permanent no-op. Reactive access must reach the instance.
|
|
32
|
+
const kc = useKeycloak();
|
|
33
|
+
const wrapper = mount(App, { global: { stubs: { RouterView: true } } });
|
|
34
|
+
const btn = wrapper.findAll('goa-button').find((b) => b.text().includes('Sign in'));
|
|
35
|
+
await btn?.trigger('_click');
|
|
36
|
+
expect(kc.keycloak?.login).toHaveBeenCalled();
|
|
37
|
+
});
|
|
27
38
|
});
|
|
@@ -2,24 +2,28 @@
|
|
|
2
2
|
import { useKeycloak } from '@dsb-norge/vue-keycloak-js';
|
|
3
3
|
import { RouterView } from 'vue-router';
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
// Keep the reactive instance — do NOT destructure. useKeycloak() returns a
|
|
6
|
+
// readonly(reactive()); destructuring snapshots each field at setup time, so
|
|
7
|
+
// `keycloak` would be frozen at its initial `undefined` (login() a no-op) and
|
|
8
|
+
// `authenticated` would never flip. Access fields on `kc` to stay reactive.
|
|
9
|
+
const kc = useKeycloak();
|
|
6
10
|
|
|
7
|
-
function login() { keycloak?.login(); }
|
|
8
|
-
function logout() { keycloak?.logout({ redirectUri: window.location.origin }); }
|
|
11
|
+
function login() { kc.keycloak?.login(); }
|
|
12
|
+
function logout() { kc.keycloak?.logout({ redirectUri: window.location.origin }); }
|
|
9
13
|
</script>
|
|
10
14
|
|
|
11
15
|
<template>
|
|
12
16
|
<goa-microsite-header type="alpha" />
|
|
13
17
|
<goa-app-header url="/" heading="<%= projectName %>">
|
|
14
18
|
<goa-button-group alignment="end">
|
|
15
|
-
<span v-if="authenticated && fullName">{{ fullName }}</span>
|
|
16
|
-
<!-- Gate on !authenticated only,
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
<goa-button v-if="!authenticated" type="tertiary" @_click="login">
|
|
19
|
+
<span v-if="kc.authenticated && kc.fullName">{{ kc.fullName }}</span>
|
|
20
|
+
<!-- Gate on !authenticated only, never on `ready`: with no load-time SSO
|
|
21
|
+
check, `ready` flips true immediately but stays informational — the
|
|
22
|
+
sign-in affordance must always show until the user is authenticated. -->
|
|
23
|
+
<goa-button v-if="!kc.authenticated" type="tertiary" @_click="login">
|
|
20
24
|
Sign in
|
|
21
25
|
</goa-button>
|
|
22
|
-
<goa-button v-if="authenticated" type="tertiary" @_click="logout">
|
|
26
|
+
<goa-button v-if="kc.authenticated" type="tertiary" @_click="logout">
|
|
23
27
|
Sign out
|
|
24
28
|
</goa-button>
|
|
25
29
|
</goa-button-group>
|
|
@@ -18,10 +18,23 @@ app.use(keycloakPlugin, {
|
|
|
18
18
|
realm: environment.access.realm,
|
|
19
19
|
clientId: environment.access.client_id,
|
|
20
20
|
},
|
|
21
|
+
// No load-time SSO check and no hidden iframes. keycloak-js's silent-SSO
|
|
22
|
+
// (silentCheckSsoRedirectUri) and login-status (checkLoginIframe) iframes both
|
|
23
|
+
// wait on an untimed postMessage that never arrives when third-party cookies are
|
|
24
|
+
// blocked (common cross-origin, e.g. behind the ADSP gateway) — init() then
|
|
25
|
+
// hangs forever, the wrapper never populates its `keycloak` ref, and login()
|
|
26
|
+
// stays a permanent no-op. So we disable every iframe: checkLoginIframe:false
|
|
27
|
+
// drops the login-status iframe, omitting silentCheckSsoRedirectUri drops the
|
|
28
|
+
// silent iframe (that combination also skips the 3p-cookie probe iframe), and an
|
|
29
|
+
// empty onLoad skips the load-time check entirely — the wrapper requires onLoad
|
|
30
|
+
// to be a string, and keycloak-js treats a falsy onLoad as "no check". Result:
|
|
31
|
+
// init settles immediately as anonymous and Sign in redirects on demand. To
|
|
32
|
+
// auto-detect an existing session where the environment allows it, set
|
|
33
|
+
// onLoad: 'check-sso' (adds a prompt=none redirect round-trip on every load).
|
|
21
34
|
init: {
|
|
22
|
-
onLoad: '
|
|
35
|
+
onLoad: '',
|
|
23
36
|
pkceMethod: 'S256',
|
|
24
|
-
|
|
37
|
+
checkLoginIframe: false,
|
|
25
38
|
},
|
|
26
39
|
});
|
|
27
40
|
|
|
@@ -16,10 +16,12 @@ const router = createRouter({
|
|
|
16
16
|
|
|
17
17
|
router.beforeEach((to) => {
|
|
18
18
|
if (to.meta.requiresAuth) {
|
|
19
|
-
|
|
20
|
-
if
|
|
21
|
-
|
|
22
|
-
|
|
19
|
+
// Read fields off the reactive instance (don't destructure) — consistent with
|
|
20
|
+
// the views and safe if this ever moves out of the per-navigation callback.
|
|
21
|
+
const kc = useKeycloak();
|
|
22
|
+
if (!kc.ready) return true;
|
|
23
|
+
if (!kc.authenticated) {
|
|
24
|
+
kc.keycloak?.login({ redirectUri: window.location.origin + to.fullPath });
|
|
23
25
|
return false;
|
|
24
26
|
}
|
|
25
27
|
}
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import { ref, onMounted } from 'vue';
|
|
2
|
+
import { ref, onMounted, watch } from 'vue';
|
|
3
3
|
import { useKeycloak } from '@dsb-norge/vue-keycloak-js';
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
// Keep the reactive instance — do NOT destructure (see App.vue): a destructured
|
|
6
|
+
// `authenticated`/`keycloak` freezes at its setup-time value, so the private
|
|
7
|
+
// fetch below would never see the user sign in and the bearer token stays undefined.
|
|
8
|
+
const kc = useKeycloak();
|
|
6
9
|
const publicResource = ref('Not retrieved — is the backend service running?');
|
|
7
10
|
const privateResource = ref('Not retrieved — sign in first.');
|
|
8
11
|
|
|
@@ -14,20 +17,27 @@ onMounted(async () => {
|
|
|
14
17
|
} catch {
|
|
15
18
|
publicResource.value = 'Error loading public resource.';
|
|
16
19
|
}
|
|
20
|
+
});
|
|
17
21
|
|
|
18
|
-
|
|
22
|
+
// React to authentication (which settles asynchronously and may flip after mount,
|
|
23
|
+
// e.g. on return from the login redirect) rather than reading it once on mount.
|
|
24
|
+
watch(
|
|
25
|
+
() => kc.authenticated,
|
|
26
|
+
async (authenticated) => {
|
|
27
|
+
if (!authenticated) return;
|
|
19
28
|
try {
|
|
20
|
-
await keycloak?.updateToken(30);
|
|
29
|
+
await kc.keycloak?.updateToken(30);
|
|
21
30
|
const res = await fetch('/api/v1/private', {
|
|
22
|
-
headers: { Authorization: `Bearer ${keycloak?.token}` },
|
|
31
|
+
headers: { Authorization: `Bearer ${kc.keycloak?.token}` },
|
|
23
32
|
});
|
|
24
33
|
const data = await res.json();
|
|
25
34
|
privateResource.value = data.message;
|
|
26
35
|
} catch {
|
|
27
36
|
privateResource.value = 'Error loading private resource.';
|
|
28
37
|
}
|
|
29
|
-
}
|
|
30
|
-
}
|
|
38
|
+
},
|
|
39
|
+
{ immediate: true }
|
|
40
|
+
);
|
|
31
41
|
</script>
|
|
32
42
|
|
|
33
43
|
<template>
|
|
@@ -1,14 +1,18 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
+
import { computed } from 'vue';
|
|
2
3
|
import { useKeycloak } from '@dsb-norge/vue-keycloak-js';
|
|
3
4
|
|
|
4
|
-
|
|
5
|
-
|
|
5
|
+
// Keep the reactive instance — do NOT destructure (see App.vue): fullName and
|
|
6
|
+
// tokenParsed populate asynchronously once Keycloak settles, so a destructured
|
|
7
|
+
// snapshot would freeze them at undefined and the page would render blank.
|
|
8
|
+
const kc = useKeycloak();
|
|
9
|
+
const userEmail = computed(() => kc.tokenParsed?.email ?? '');
|
|
6
10
|
</script>
|
|
7
11
|
|
|
8
12
|
<template>
|
|
9
13
|
<section>
|
|
10
14
|
<h2>Protected page</h2>
|
|
11
|
-
<p>You are signed in as <strong>{{ fullName }}</strong> ({{ userEmail }}).</p>
|
|
15
|
+
<p>You are signed in as <strong>{{ kc.fullName }}</strong> ({{ userEmail }}).</p>
|
|
12
16
|
<router-link to="/">← Back to home</router-link>
|
|
13
17
|
</section>
|
|
14
18
|
</template>
|
|
@@ -66,8 +66,9 @@ function default_1(host, options) {
|
|
|
66
66
|
(0, devkit_1.addDependenciesToPackageJson)(host, {
|
|
67
67
|
'@abgov/design-tokens': '1.8.0',
|
|
68
68
|
'@abgov/web-components': '1.39.3',
|
|
69
|
+
// keycloak-js is a transitive dependency of @dsb-norge/vue-keycloak-js;
|
|
70
|
+
// don't pin it directly or the versions diverge into two copies.
|
|
69
71
|
'@dsb-norge/vue-keycloak-js': '^3.0.0',
|
|
70
|
-
'keycloak-js': '^23.0.7',
|
|
71
72
|
'pinia': '^2.0.0',
|
|
72
73
|
'vue-router': '^4.0.0',
|
|
73
74
|
}, {
|
|
@@ -101,8 +102,8 @@ function default_1(host, options) {
|
|
|
101
102
|
if (addedProxy && ((_a = config.targets.serve) === null || _a === void 0 ? void 0 : _a.options)) {
|
|
102
103
|
config.targets.serve.options = Object.assign(Object.assign({}, config.targets.serve.options), { proxyConfig: `${normalizedOptions.projectRoot}/vite.proxy.json` });
|
|
103
104
|
}
|
|
104
|
-
// nginx.conf
|
|
105
|
-
// (<projectRoot>/public) so
|
|
105
|
+
// nginx.conf lives in the Vite publicDir
|
|
106
|
+
// (<projectRoot>/public) so it's emitted to the build output root — the
|
|
106
107
|
// @nx/vite:build executor ignores webpack-style `assets`. Pin outputPath to
|
|
107
108
|
// the workspace-root dist so it matches the vite config's outDir and the
|
|
108
109
|
// generated Dockerfile's COPY path.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"vue-app.js","sourceRoot":"","sources":["../../../../../../packages/nx-adsp/src/generators/vue-app/vue-app.ts"],"names":[],"mappings":";;AAgEA,
|
|
1
|
+
{"version":3,"file":"vue-app.js","sourceRoot":"","sources":["../../../../../../packages/nx-adsp/src/generators/vue-app/vue-app.ts"],"names":[],"mappings":"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"}
|
|
@@ -36,9 +36,8 @@ describe('Vue App Generator', () => {
|
|
|
36
36
|
await generator(host, options);
|
|
37
37
|
const config = readProjectConfiguration(host, 'test');
|
|
38
38
|
expect(config.root).toBe('apps/test');
|
|
39
|
-
// nginx.conf
|
|
39
|
+
// nginx.conf lives in the Vite publicDir so it ends up in the build output.
|
|
40
40
|
expect(host.exists('apps/test/public/nginx.conf')).toBeTruthy();
|
|
41
|
-
expect(host.exists('apps/test/public/silent-check-sso.html')).toBeTruthy();
|
|
42
41
|
expect(host.exists('apps/test/src/main.ts')).toBeTruthy();
|
|
43
42
|
expect(host.exists('apps/test/src/App.vue')).toBeTruthy();
|
|
44
43
|
expect(host.exists('apps/test/src/router/index.ts')).toBeTruthy();
|
|
@@ -50,6 +49,48 @@ describe('Vue App Generator', () => {
|
|
|
50
49
|
expect(config.targets.build.options.outputPath).toBe('dist/apps/test');
|
|
51
50
|
}, 30000);
|
|
52
51
|
|
|
52
|
+
it('inits Keycloak with no hidden iframes so init never hangs', async () => {
|
|
53
|
+
await generator(host, options);
|
|
54
|
+
// Strip // comments — they legitimately reference the disabled iframe options
|
|
55
|
+
// to explain their absence; assert against the actual init code.
|
|
56
|
+
const code = host
|
|
57
|
+
.read('apps/test/src/main.ts')
|
|
58
|
+
.toString()
|
|
59
|
+
.split('\n')
|
|
60
|
+
.filter((l) => !l.trim().startsWith('//'))
|
|
61
|
+
.join('\n');
|
|
62
|
+
// keycloak-js's silent-SSO (silentCheckSsoRedirectUri) and login-status
|
|
63
|
+
// (checkLoginIframe) iframes both wait on an untimed postMessage that hangs
|
|
64
|
+
// when third-party cookies are blocked, leaving keycloak.login() a no-op. We
|
|
65
|
+
// disable both and skip the load-time check (empty onLoad) so init settles.
|
|
66
|
+
expect(code).not.toContain('silentCheckSsoRedirectUri');
|
|
67
|
+
expect(code).toContain('checkLoginIframe: false');
|
|
68
|
+
expect(code).toContain("onLoad: ''");
|
|
69
|
+
expect(code).toContain("pkceMethod: 'S256'");
|
|
70
|
+
// the now-unused silent-check-sso.html is no longer generated
|
|
71
|
+
expect(host.exists('apps/test/public/silent-check-sso.html')).toBeFalsy();
|
|
72
|
+
}, 30000);
|
|
73
|
+
|
|
74
|
+
it('reads Keycloak fields off the reactive instance without destructuring', async () => {
|
|
75
|
+
await generator(host, options);
|
|
76
|
+
// The Sign in no-op bug: destructuring useKeycloak() (a readonly(reactive()))
|
|
77
|
+
// froze `keycloak` at undefined so login() never fired. Every consumer must
|
|
78
|
+
// keep the instance and read fields off it.
|
|
79
|
+
for (const file of [
|
|
80
|
+
'apps/test/src/App.vue',
|
|
81
|
+
'apps/test/src/views/HomeView.vue',
|
|
82
|
+
'apps/test/src/views/ProtectedView.vue',
|
|
83
|
+
'apps/test/src/router/index.ts',
|
|
84
|
+
]) {
|
|
85
|
+
const code = host.read(file).toString();
|
|
86
|
+
expect(code).toContain('= useKeycloak()');
|
|
87
|
+
// no destructuring assignment off useKeycloak()
|
|
88
|
+
expect(code).not.toMatch(/const\s*\{[^}]*\}\s*=\s*useKeycloak\(\)/);
|
|
89
|
+
}
|
|
90
|
+
const app = host.read('apps/test/src/App.vue').toString();
|
|
91
|
+
expect(app).toContain('kc.keycloak?.login()');
|
|
92
|
+
}, 30000);
|
|
93
|
+
|
|
53
94
|
it('index.html is at the Vite entry root and its mount target matches main.ts', async () => {
|
|
54
95
|
await generator(host, options);
|
|
55
96
|
// Vite's entry is <projectRoot>/index.html, not src/index.html — a template
|
package/src/utils/quality.js
CHANGED
|
@@ -57,7 +57,11 @@ function addJestCoverageConfig(host, projectRoot) {
|
|
|
57
57
|
if (!host.exists(jestPath))
|
|
58
58
|
return;
|
|
59
59
|
const existing = host.read(jestPath).toString();
|
|
60
|
-
|
|
60
|
+
// Capture the coverageDirectory line WITHOUT its trailing comma, then re-emit
|
|
61
|
+
// it with one before the inserted properties. @nx/jest may leave
|
|
62
|
+
// coverageDirectory as the last property (no trailing comma), so appending
|
|
63
|
+
// properties after it verbatim would produce an invalid object literal.
|
|
64
|
+
const modified = existing.replace(/([ \t]*coverageDirectory:[^\n]*?),?\n/, `$1,\n collectCoverage: true,\n coverageThreshold: {\n global: {\n lines: 60,\n },\n },\n`);
|
|
61
65
|
if (modified !== existing) {
|
|
62
66
|
host.write(jestPath, modified);
|
|
63
67
|
}
|
package/src/utils/quality.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"quality.js","sourceRoot":"","sources":["../../../../../packages/nx-adsp/src/utils/quality.ts"],"names":[],"mappings":";;AAUA,sDAoCC;AAOD,
|
|
1
|
+
{"version":3,"file":"quality.js","sourceRoot":"","sources":["../../../../../packages/nx-adsp/src/utils/quality.ts"],"names":[],"mappings":";;AAUA,sDAoCC;AAOD,sDAgBC;AAOD,4CAeC;AAED,8CAeC;AA5GD,uCAA+G;AAE/G;;;;;;;GAOG;AACH,SAAgB,qBAAqB,CAAC,IAAU,EAAE,WAAmB,EAAE,aAAuB;;IAC5F,MAAM,UAAU,GAAG,GAAG,WAAW,oBAAoB,CAAC;IACtD,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,UAAU,CAAC;QAAE,OAAO;IAErC,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC,QAAQ,EAAE,CAAC;IAClD,MAAM,eAAe,GAAG,QAAQ,CAAC,KAAK,CAAC,6CAA6C,CAAC,CAAC;IACtF,MAAM,cAAc,GAAG,MAAA,eAAe,aAAf,eAAe,uBAAf,eAAe,CAAG,CAAC,CAAC,mCAAI,yBAAyB,CAAC;IACzE,MAAM,YAAY,GAAG,aAAa,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAEnE,IAAI,CAAC,KAAK,CACR,UAAU,EACV,2BAA2B,cAAc;;;;;;;;;;;;;;;cAe/B,YAAY;;;;;;;;CAQzB,CACE,CAAC;AACJ,CAAC;AAED;;;;GAIG;AACH,SAAgB,qBAAqB,CAAC,IAAU,EAAE,WAAmB;IACnE,MAAM,QAAQ,GAAG,GAAG,WAAW,kBAAkB,CAAC;IAClD,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC;QAAE,OAAO;IAEnC,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,QAAQ,EAAE,CAAC;IAChD,8EAA8E;IAC9E,iEAAiE;IACjE,2EAA2E;IAC3E,wEAAwE;IACxE,MAAM,QAAQ,GAAG,QAAQ,CAAC,OAAO,CAC/B,uCAAuC,EACvC,wGAAwG,CACzG,CAAC;IACF,IAAI,QAAQ,KAAK,QAAQ,EAAE,CAAC;QAC1B,IAAI,CAAC,KAAK,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC;IACjC,CAAC;AACH,CAAC;AAED;;;;GAIG;AACH,SAAgB,gBAAgB,CAAC,IAAU,EAAE,WAAmB;IAC9D,MAAM,MAAM,GAAG,IAAA,iCAAwB,EAAC,IAAI,EAAE,WAAW,CAAC,CAAC;IAC3D,MAAM,CAAC,OAAO,mCACT,MAAM,CAAC,OAAO,KACjB,OAAO,EAAE;YACP,QAAQ,EAAE,iBAAiB;YAC3B,MAAM,EAAE,CAAC,SAAS,CAAC;YACnB,KAAK,EAAE,IAAI;YACX,OAAO,EAAE;gBACP,OAAO,EAAE,iDAAiD;gBAC1D,GAAG,EAAE,eAAe;aACrB;SACF,GACF,CAAC;IACF,IAAA,mCAA0B,EAAC,IAAI,EAAE,WAAW,EAAE,MAAM,CAAC,CAAC;AACxD,CAAC;AAED,SAAgB,iBAAiB,CAAC,IAAU;IAC1C,MAAM,YAAY,GAAG,uBAAuB,CAAC;IAC7C,MAAM,QAAQ,GAAG;QACf,qBAAqB,EAAE,IAAI;QAC3B,yBAAyB,EAAE,wBAAwB;QACnD,0BAA0B,EAAE;YAC1B,sBAAsB,EAAE,UAAU;SACnC;KACF,CAAC;IAEF,IAAI,IAAI,CAAC,MAAM,CAAC,YAAY,CAAC,EAAE,CAAC;QAC9B,IAAA,mBAAU,EAAC,IAAI,EAAE,YAAY,EAAE,CAAC,QAAQ,EAAE,EAAE,CAAC,iCAAM,QAAQ,GAAK,QAAQ,EAAG,CAAC,CAAC;IAC/E,CAAC;SAAM,CAAC;QACN,IAAA,kBAAS,EAAC,IAAI,EAAE,YAAY,EAAE,QAAQ,CAAC,CAAC;IAC1C,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { Tree } from '@nx/devkit';
|
|
2
|
+
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
|
|
3
|
+
import { addJestCoverageConfig } from './quality';
|
|
4
|
+
|
|
5
|
+
function makeConfig(coverageDirectoryLine: string): string {
|
|
6
|
+
return [
|
|
7
|
+
'module.exports = {',
|
|
8
|
+
" displayName: 'svc',",
|
|
9
|
+
" preset: '../../jest.preset.js',",
|
|
10
|
+
` ${coverageDirectoryLine}`,
|
|
11
|
+
'};',
|
|
12
|
+
'',
|
|
13
|
+
].join('\n');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Parse the exported object literal; throws SyntaxError if the config is invalid
|
|
17
|
+
// (e.g. a property with no separating comma).
|
|
18
|
+
function evalConfigObject(src: string): Record<string, unknown> {
|
|
19
|
+
const body = src.slice(src.indexOf('{'), src.lastIndexOf('}') + 1);
|
|
20
|
+
return new Function(`return (${body})`)() as Record<string, unknown>;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
describe('addJestCoverageConfig', () => {
|
|
24
|
+
let tree: Tree;
|
|
25
|
+
beforeEach(() => {
|
|
26
|
+
tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' });
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('inserts a comma when coverageDirectory is the last property (no trailing comma)', () => {
|
|
30
|
+
// The @nx/jest (Nx 23) template leaves coverageDirectory without a trailing comma.
|
|
31
|
+
tree.write(
|
|
32
|
+
'apps/svc/jest.config.cts',
|
|
33
|
+
makeConfig("coverageDirectory: 'test-output/jest/coverage'")
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
addJestCoverageConfig(tree, 'apps/svc');
|
|
37
|
+
const out = tree.read('apps/svc/jest.config.cts').toString();
|
|
38
|
+
|
|
39
|
+
expect(out).toContain("coverageDirectory: 'test-output/jest/coverage',");
|
|
40
|
+
expect(out).toContain('collectCoverage: true,');
|
|
41
|
+
expect(out).toContain('lines: 60,');
|
|
42
|
+
expect(out).not.toContain(',,');
|
|
43
|
+
// The result must be a valid object literal.
|
|
44
|
+
const cfg = evalConfigObject(out);
|
|
45
|
+
expect(cfg.collectCoverage).toBe(true);
|
|
46
|
+
expect((cfg.coverageThreshold as { global: { lines: number } }).global.lines).toBe(60);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('does not double the comma when coverageDirectory already has one', () => {
|
|
50
|
+
tree.write(
|
|
51
|
+
'apps/svc/jest.config.cts',
|
|
52
|
+
makeConfig("coverageDirectory: '../../coverage/svc',")
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
addJestCoverageConfig(tree, 'apps/svc');
|
|
56
|
+
const out = tree.read('apps/svc/jest.config.cts').toString();
|
|
57
|
+
|
|
58
|
+
expect(out).toContain("coverageDirectory: '../../coverage/svc',");
|
|
59
|
+
expect(out).not.toContain(',,');
|
|
60
|
+
expect(() => evalConfigObject(out)).not.toThrow();
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('is a no-op when there is no jest config', () => {
|
|
64
|
+
expect(() => addJestCoverageConfig(tree, 'apps/none')).not.toThrow();
|
|
65
|
+
expect(tree.exists('apps/none/jest.config.cts')).toBe(false);
|
|
66
|
+
});
|
|
67
|
+
});
|