@keycloakify/svelte 0.1.3 → 0.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.
Files changed (46) hide show
  1. package/keycloakify-svelte/bin/279.index.js +194 -48
  2. package/keycloakify-svelte/login/DefaultPage.svelte.d.ts +4 -4
  3. package/keycloakify-svelte/login/components/AddRemoveButtonsMultiValuedAttribute.svelte.d.ts +4 -3
  4. package/keycloakify-svelte/login/components/FieldErrors.svelte.d.ts +3 -3
  5. package/keycloakify-svelte/login/components/GroupLabel.svelte.d.ts +4 -4
  6. package/keycloakify-svelte/login/components/InputTag.svelte.d.ts +1 -1
  7. package/keycloakify-svelte/login/components/LogoutOtherSessions.svelte.d.ts +3 -3
  8. package/keycloakify-svelte/login/components/PasswordWrapper.svelte.d.ts +4 -4
  9. package/keycloakify-svelte/login/components/TermsAcceptance.svelte.d.ts +4 -3
  10. package/keycloakify-svelte/login/pages/IdpReviewUserProfile.svelte.d.ts +5 -2
  11. package/keycloakify-svelte/login/pages/LoginUpdateProfile.svelte.d.ts +5 -2
  12. package/keycloakify-svelte/login/pages/Register.svelte.d.ts +5 -2
  13. package/keycloakify-svelte/login/pages/UpdateEmail.svelte.d.ts +5 -2
  14. package/package.json +26 -24
  15. package/src/bin/add-story.ts +117 -0
  16. package/src/bin/core.ts +10 -0
  17. package/src/bin/eject-page.ts +233 -0
  18. package/src/bin/initialize-account-theme/boilerplate/KcContext.ts +11 -0
  19. package/src/bin/initialize-account-theme/boilerplate/KcPage.svelte +28 -0
  20. package/src/bin/initialize-account-theme/boilerplate/KcPageStory.svelte +9 -0
  21. package/src/bin/initialize-account-theme/boilerplate/KcPageStory.ts +22 -0
  22. package/src/bin/initialize-account-theme/boilerplate/i18n.ts +9 -0
  23. package/src/bin/initialize-account-theme/index.ts +1 -0
  24. package/src/bin/initialize-account-theme/initialize-account-theme.ts +87 -0
  25. package/src/bin/initialize-account-theme/updateAccountThemeImplementationInConfig.ts +93 -0
  26. package/src/bin/main.ts +43 -0
  27. package/src/bin/tools/SemVer.ts +107 -0
  28. package/src/bin/tools/String.prototype.replaceAll.ts +31 -0
  29. package/src/bin/tools/crawl.ts +32 -0
  30. package/src/bin/tools/fs.rmSync.ts +34 -0
  31. package/src/bin/tools/getThisCodebaseRootDirPath.ts +22 -0
  32. package/src/bin/tools/kebabCaseToSnakeCase.ts +7 -0
  33. package/src/bin/tools/nodeModulesBinDirPath.ts +38 -0
  34. package/src/bin/tools/readThisNpmPackageVersion.ts +22 -0
  35. package/src/bin/tools/runPrettier.ts +118 -0
  36. package/src/bin/tools/transformCodebase.ts +81 -0
  37. package/src/bin/tools/transformCodebase_async.ts +82 -0
  38. package/src/bin/tsconfig.json +23 -0
  39. package/src/bin/update-kc-gen.ts +153 -0
  40. package/src/tools/useConst.ts +4 -0
  41. package/src/tools/useInsertLinkTags.ts +81 -0
  42. package/src/tools/useInsertScriptTags.ts +110 -0
  43. package/src/tools/useReducer.ts +11 -0
  44. package/src/tools/useSetClassName.ts +18 -0
  45. package/src/tools/useState.ts +8 -0
  46. package/stories/login/KcPage.svelte +10 -10
@@ -0,0 +1,153 @@
1
+ import * as crypto from 'crypto';
2
+ import * as fs from 'fs';
3
+ import { join as pathJoin } from 'path';
4
+ import type { BuildContext } from './core';
5
+ import { getIsPrettierAvailable, runPrettier } from './tools/runPrettier';
6
+
7
+ export async function command(params: { buildContext: BuildContext }) {
8
+ const { buildContext } = params;
9
+
10
+ const filePath = pathJoin(buildContext.themeSrcDirPath, 'kc.gen.ts');
11
+ const svelteFilePath = pathJoin(buildContext.themeSrcDirPath, 'kc.gen.svelte');
12
+
13
+ const hasLoginTheme = buildContext.implementedThemeTypes.login.isImplemented;
14
+ const hasAccountTheme = buildContext.implementedThemeTypes.account.isImplemented;
15
+ const hasAdminTheme = buildContext.implementedThemeTypes.admin.isImplemented;
16
+
17
+ const newContent = [
18
+ `/* eslint-disable */`,
19
+ ``,
20
+ `// @ts-nocheck`,
21
+ ``,
22
+ `// noinspection JSUnusedGlobalSymbols`,
23
+ ``,
24
+ `export type ThemeName = ${buildContext.themeNames.map((themeName) => `"${themeName}"`).join(' | ')};`,
25
+ ``,
26
+ `export const themeNames: ThemeName[] = [${buildContext.themeNames.map((themeName) => `"${themeName}"`).join(', ')}];`,
27
+ ``,
28
+ `export type KcEnvName = ${buildContext.environmentVariables.length === 0 ? 'never' : buildContext.environmentVariables.map(({ name }) => `"${name}"`).join(' | ')};`,
29
+ ``,
30
+ `export const kcEnvNames: KcEnvName[] = [${buildContext.environmentVariables.map(({ name }) => `"${name}"`).join(', ')}];`,
31
+ ``,
32
+ `export const kcEnvDefaults: Record<KcEnvName, string> = ${JSON.stringify(
33
+ Object.fromEntries(
34
+ buildContext.environmentVariables.map(({ name, default: defaultValue }) => [name, defaultValue]),
35
+ ),
36
+ null,
37
+ 2,
38
+ )};`,
39
+ ``,
40
+ `export type KcContext =`,
41
+ hasLoginTheme && ` | import("./login/KcContext").KcContext`,
42
+ hasAccountTheme && ` | import("./account/KcContext").KcContext`,
43
+ hasAdminTheme && ` | import("./admin/KcContext").KcContext`,
44
+ ``,
45
+ `declare global {`,
46
+ ` interface Window {`,
47
+ ` kcContext?: KcContext;`,
48
+ ` }`,
49
+ `}`,
50
+ ``,
51
+ ]
52
+ .filter((item) => typeof item === 'string')
53
+ .join('\n');
54
+
55
+ const newSvelteContent = [
56
+ ` import type { Component } from 'svelte';`,
57
+ ` import type { KcContext } from './kc.gen';`,
58
+ ``,
59
+ ` const { kcContext, Fallback }: { kcContext: KcContext; Fallback?: Component } = $props();`,
60
+ ``,
61
+ hasLoginTheme && ` const KcLoginPage = import("./login/KcPage.svelte");`,
62
+ hasAccountTheme && ` const KcAccountPage = import("./account/KcPage.svelte");`,
63
+ hasAdminTheme && ` const KcAdminPage = import("./admin/KcPage.svelte");`,
64
+ `</script>`,
65
+ ``,
66
+ `{#if kcContext.themeType === 'login'}`,
67
+ hasLoginTheme && ` {#await KcLoginPage}`,
68
+ hasLoginTheme && ` {#if Fallback}`,
69
+ hasLoginTheme && ` <Fallback></Fallback>`,
70
+ hasLoginTheme && ` {/if}`,
71
+ hasLoginTheme && ` {:then { default: KcPage }}`,
72
+ hasLoginTheme && ` <KcPage {kcContext} />`,
73
+ hasLoginTheme && ` {/await}`,
74
+ !hasLoginTheme && ` <!-- login not implemented -->`,
75
+ `{:else if kcContext.themeType === 'account'}`,
76
+ hasAccountTheme && ` {#await KcAccountPage}`,
77
+ hasAccountTheme && ` {#if Fallback}`,
78
+ hasAccountTheme && ` <Fallback></Fallback>`,
79
+ hasAccountTheme && ` {/if}`,
80
+ hasAccountTheme && ` {:then { default: KcPage }}`,
81
+ hasAccountTheme && ` <KcPage {kcContext} />`,
82
+ hasAccountTheme && ` {/await}`,
83
+ !hasAccountTheme && ` <!-- account not implemented -->`,
84
+ // TODO: admin theme
85
+ `{/if}`,
86
+ ]
87
+ .filter((item) => typeof item === 'string')
88
+ .join('\n');
89
+
90
+ const hash = crypto.createHash('sha256').update(newContent).digest('hex');
91
+ const svelteHash = crypto.createHash('sha256').update(newSvelteContent).digest('hex');
92
+
93
+ skip_if_no_changes: {
94
+ if (!fs.existsSync(filePath)) {
95
+ break skip_if_no_changes;
96
+ }
97
+
98
+ const currentContent = fs.readFileSync(filePath).toString('utf8');
99
+
100
+ if (!currentContent.includes(hash)) {
101
+ break skip_if_no_changes;
102
+ }
103
+
104
+ return;
105
+ }
106
+
107
+ skip_if_no_svelte_changes: {
108
+ if (!fs.existsSync(svelteFilePath)) {
109
+ break skip_if_no_svelte_changes;
110
+ }
111
+
112
+ const currentContent = fs.readFileSync(svelteFilePath).toString('utf8');
113
+
114
+ if (!currentContent.includes(svelteHash)) {
115
+ break skip_if_no_svelte_changes;
116
+ }
117
+
118
+ return;
119
+ }
120
+
121
+ let sourceCode = [
122
+ `// This file is auto-generated by keycloakify. Do not edit it manually.`,
123
+ `// Hash: ${hash}`,
124
+ ``,
125
+ newContent,
126
+ ].join('\n');
127
+
128
+ let svelteSourceCode = [
129
+ `<script lang="ts">`,
130
+ `// This file is auto-generated by keycloakify. Do not edit it manually.`,
131
+ `// Hash: ${svelteHash}`,
132
+ ``,
133
+ newSvelteContent,
134
+ ].join('\n');
135
+
136
+ run_prettier: {
137
+ if (!(await getIsPrettierAvailable())) {
138
+ break run_prettier;
139
+ }
140
+
141
+ sourceCode = await runPrettier({
142
+ filePath,
143
+ sourceCode,
144
+ });
145
+ svelteSourceCode = await runPrettier({
146
+ filePath: svelteFilePath,
147
+ sourceCode: svelteSourceCode,
148
+ });
149
+ }
150
+
151
+ fs.writeFileSync(filePath, Buffer.from(sourceCode, 'utf8'));
152
+ fs.writeFileSync(svelteFilePath, Buffer.from(svelteSourceCode, 'utf8'));
153
+ }
@@ -0,0 +1,4 @@
1
+ export function useConst<T>(getValue: () => T): T {
2
+ const value = getValue();
3
+ return value;
4
+ }
@@ -0,0 +1,81 @@
1
+ import { onMount } from 'svelte';
2
+ import { id } from 'tsafe/id';
3
+ import { useConst } from '@keycloakify/svelte/tools/useConst';
4
+ import { useReducer } from '@keycloakify/svelte/tools/useReducer';
5
+
6
+ const alreadyMountedComponentOrHookNames = new Set<string>();
7
+
8
+ /**
9
+ * NOTE: The component that use this hook can only be mounded once!
10
+ * And can't rerender with different hrefs.
11
+ * If it's mounted again the page will be reloaded.
12
+ * This simulates the behavior of a server rendered page that imports css stylesheet in the head.
13
+ */
14
+ export function useInsertLinkTags(params: { componentOrHookName: string; hrefs: string[] }) {
15
+ const { hrefs, componentOrHookName } = params;
16
+
17
+ onMount(() => {
18
+ const isAlreadyMounted = alreadyMountedComponentOrHookNames.has(componentOrHookName);
19
+
20
+ if (isAlreadyMounted) {
21
+ reload: {
22
+ if (new URL(window.location.href).searchParams.get('viewMode') === 'docs') {
23
+ // NOTE: Special case for Storybook, we want to avoid infinite reload loop.
24
+ break reload;
25
+ }
26
+ window.location.reload();
27
+ }
28
+ return;
29
+ }
30
+
31
+ alreadyMountedComponentOrHookNames.add(componentOrHookName);
32
+ });
33
+
34
+ const [areAllStyleSheetsLoaded, setAllStyleSheetsLoaded] = useReducer<boolean, void>(() => true, false);
35
+
36
+ const refPrAllStyleSheetLoaded = useConst(() => ({
37
+ current: id<Promise<void> | undefined>(undefined),
38
+ }));
39
+
40
+ onMount(() => {
41
+ let isActive = true;
42
+
43
+ (refPrAllStyleSheetLoaded.current ??= (async () => {
44
+ let lastMountedHtmlElement: HTMLLinkElement | undefined = undefined;
45
+
46
+ const prs: Promise<void>[] = [];
47
+
48
+ for (const href of hrefs) {
49
+ const htmlElement = document.createElement('link');
50
+
51
+ prs.push(new Promise<void>((resolve) => htmlElement.addEventListener('load', () => resolve())));
52
+
53
+ htmlElement.rel = 'stylesheet';
54
+
55
+ htmlElement.href = href;
56
+
57
+ if (lastMountedHtmlElement !== undefined) {
58
+ lastMountedHtmlElement.insertAdjacentElement('afterend', htmlElement);
59
+ } else {
60
+ document.head.prepend(htmlElement);
61
+ }
62
+
63
+ lastMountedHtmlElement = htmlElement;
64
+ }
65
+
66
+ await Promise.all(prs);
67
+ })()).then(() => {
68
+ if (!isActive) {
69
+ return;
70
+ }
71
+
72
+ setAllStyleSheetsLoaded();
73
+ });
74
+
75
+ return () => {
76
+ isActive = false;
77
+ };
78
+ });
79
+
80
+ return { areAllStyleSheetsLoaded };
81
+ }
@@ -0,0 +1,110 @@
1
+ /* eslint-disable @typescript-eslint/no-namespace */
2
+ import { onMount } from 'svelte';
3
+ import { assert } from 'tsafe/assert';
4
+
5
+ export type ScriptTag = ScriptTag.TextContent | ScriptTag.Src;
6
+
7
+ export namespace ScriptTag {
8
+ type Common = {
9
+ type: 'text/javascript' | 'module';
10
+ };
11
+
12
+ export type TextContent = Common & {
13
+ textContent: string | (() => string);
14
+ };
15
+ export type Src = Common & {
16
+ src: string;
17
+ };
18
+ }
19
+
20
+ const alreadyMountedComponentOrHookNames = new Set<string>();
21
+
22
+ /**
23
+ * NOTE: The component that use this hook can only be mounded once!
24
+ * And can't rerender with different scriptTags.
25
+ * If it's mounted again the page will be reloaded.
26
+ * This simulates the behavior of a server rendered page that imports javascript in the head.
27
+ *
28
+ * The returned function is supposed to be called in a useEffect and
29
+ * will not download the scripts multiple times event if called more than once (react strict mode).
30
+ *
31
+ */
32
+ export function useInsertScriptTags(params: { componentOrHookName: string; scriptTags: ScriptTag[] }) {
33
+ const { scriptTags, componentOrHookName } = params;
34
+
35
+ onMount(() => {
36
+ const isAlreadyMounted = alreadyMountedComponentOrHookNames.has(componentOrHookName);
37
+
38
+ if (isAlreadyMounted) {
39
+ reload: {
40
+ if (new URL(window.location.href).searchParams.get('viewMode') === 'docs') {
41
+ // NOTE: Special case for Storybook, we want to avoid infinite reload loop.
42
+ break reload;
43
+ }
44
+ window.location.reload();
45
+ }
46
+ return;
47
+ }
48
+
49
+ alreadyMountedComponentOrHookNames.add(componentOrHookName);
50
+ });
51
+
52
+ let areScriptsInserted = false;
53
+
54
+ const insertScriptTags = () => {
55
+ if (areScriptsInserted) {
56
+ return;
57
+ }
58
+
59
+ scriptTags.forEach((scriptTag) => {
60
+ // NOTE: Avoid loading same script twice. (Like jQuery for example)
61
+ {
62
+ const scripts = document.getElementsByTagName('script');
63
+ for (let i = 0; i < scripts.length; i++) {
64
+ const script = scripts[i];
65
+ if ('textContent' in scriptTag) {
66
+ const textContent =
67
+ typeof scriptTag.textContent === 'function' ? scriptTag.textContent() : scriptTag.textContent;
68
+
69
+ if (script.textContent === textContent) {
70
+ return;
71
+ }
72
+ continue;
73
+ }
74
+ if ('src' in scriptTag) {
75
+ if (script.getAttribute('src') === scriptTag.src) {
76
+ return;
77
+ }
78
+ continue;
79
+ }
80
+ assert(false);
81
+ }
82
+ }
83
+
84
+ const htmlElement = document.createElement('script');
85
+
86
+ htmlElement.type = scriptTag.type;
87
+
88
+ (() => {
89
+ if ('textContent' in scriptTag) {
90
+ const textContent =
91
+ typeof scriptTag.textContent === 'function' ? scriptTag.textContent() : scriptTag.textContent;
92
+
93
+ htmlElement.textContent = textContent;
94
+ return;
95
+ }
96
+ if ('src' in scriptTag) {
97
+ htmlElement.src = scriptTag.src;
98
+ return;
99
+ }
100
+ assert(false);
101
+ })();
102
+
103
+ document.head.appendChild(htmlElement);
104
+ });
105
+
106
+ areScriptsInserted = true;
107
+ };
108
+
109
+ return { insertScriptTags };
110
+ }
@@ -0,0 +1,11 @@
1
+ import { derived, writable, type Readable } from 'svelte/store';
2
+
3
+ export const useReducer = <T, R = void>(
4
+ reducer: (state: T, action: R) => T,
5
+ initialState: T,
6
+ ): [Readable<T>, (action: R) => void] => {
7
+ const state = writable(initialState);
8
+ const dispatch = (action: R) => state.update((currentState) => reducer(currentState, action));
9
+ const readableState = derived(state, ($state) => $state);
10
+ return [readableState, dispatch];
11
+ };
@@ -0,0 +1,18 @@
1
+ import { onMount } from 'svelte';
2
+
3
+ export function useSetClassName(params: { qualifiedName: 'html' | 'body'; className: string | undefined }) {
4
+ const { qualifiedName, className } = params;
5
+ onMount(() => {
6
+ if (className && className !== '') {
7
+ const htmlClassList = document.getElementsByTagName(qualifiedName)[0].classList;
8
+
9
+ const tokens = className.split(' ');
10
+ htmlClassList.add(...tokens);
11
+
12
+ // Clean up when the component is destroyed
13
+ return () => {
14
+ htmlClassList.remove(...tokens);
15
+ };
16
+ }
17
+ });
18
+ }
@@ -0,0 +1,8 @@
1
+ import { derived, writable, type Readable } from 'svelte/store';
2
+
3
+ export const useState = <T>(initialState: T): [Readable<T>, (newState: T) => void] => {
4
+ const state = writable(initialState);
5
+ const dispatch = (newState: T) => state.set(newState);
6
+ const readableState = derived(state, ($state) => $state);
7
+ return [readableState, dispatch];
8
+ };
@@ -12,7 +12,7 @@
12
12
 
13
13
  const classes = {} satisfies { [key in ClassKey]?: string };
14
14
  const doMakeUserConfirmPassword = true;
15
-
15
+
16
16
  const page = async (): Promise<{ default?: Component<any> }> => {
17
17
  switch (kcContext.pageId) {
18
18
  default:
@@ -22,13 +22,13 @@
22
22
  </script>
23
23
 
24
24
  {#await page() then { default: Page }}
25
- <Page
26
- {kcContext}
27
- i18n={i18n}
28
- {classes}
29
- {Template}
30
- {UserProfileFormFields}
31
- doUseDefaultCss={true}
32
- {doMakeUserConfirmPassword}
33
- ></Page>
25
+ <Page
26
+ {kcContext}
27
+ {i18n}
28
+ {classes}
29
+ {Template}
30
+ {UserProfileFormFields}
31
+ doUseDefaultCss={true}
32
+ {doMakeUserConfirmPassword}
33
+ ></Page>
34
34
  {/await}