@aerogel/plugin-solid 0.0.1 → 0.1.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.
Files changed (39) hide show
  1. package/dist/InruptAuthenticator.lazy-BvpQLGoj.js +9 -0
  2. package/dist/InruptAuthenticator.lazy-BvpQLGoj.js.map +1 -0
  3. package/dist/LegacyAuthenticator.lazy-ChFFqP14.js +5 -0
  4. package/dist/LegacyAuthenticator.lazy-ChFFqP14.js.map +1 -0
  5. package/dist/aerogel-plugin-solid.d.ts +120 -92
  6. package/dist/aerogel-plugin-solid.js +724 -0
  7. package/dist/aerogel-plugin-solid.js.map +1 -0
  8. package/package.json +35 -28
  9. package/src/auth/Authenticator.ts +10 -8
  10. package/src/auth/authenticators/InruptAuthenticator.lazy.ts +1 -0
  11. package/src/auth/authenticators/InruptAuthenticator.ts +41 -12
  12. package/src/auth/authenticators/LegacyAuthenticator.lazy.ts +1 -0
  13. package/src/auth/authenticators/LegacyAuthenticator.ts +80 -0
  14. package/src/auth/index.ts +3 -1
  15. package/src/components/SolidAvatar.vue +35 -0
  16. package/src/components/SolidImage.vue +33 -0
  17. package/src/components/SolidLogin.vue +141 -0
  18. package/src/components/index.ts +3 -4
  19. package/src/errors/AuthenticationFailedError.ts +22 -0
  20. package/src/errors/index.ts +3 -4
  21. package/src/forms/validation.ts +11 -0
  22. package/src/index.css +1 -0
  23. package/src/index.test.ts +24 -0
  24. package/src/index.ts +65 -0
  25. package/src/services/Solid.state.ts +12 -16
  26. package/src/services/Solid.ts +317 -139
  27. package/src/testing/setup.ts +11 -0
  28. package/src/types/env.d.ts +1 -0
  29. package/src/types/solid-auth-client.d.ts +21 -0
  30. package/.eslintrc.js +0 -7
  31. package/dist/aerogel-plugin-solid.cjs.js +0 -2
  32. package/dist/aerogel-plugin-solid.cjs.js.map +0 -1
  33. package/dist/aerogel-plugin-solid.esm.js +0 -2
  34. package/dist/aerogel-plugin-solid.esm.js.map +0 -1
  35. package/noeldemartin.config.js +0 -2
  36. package/src/components/AGSolidStatus.ts +0 -5
  37. package/src/components/AGSolidStatus.vue +0 -60
  38. package/src/main.ts +0 -43
  39. package/tsconfig.json +0 -10
@@ -0,0 +1,141 @@
1
+ <template>
2
+ <Form :form :class="renderedClasses" @submit="submit">
3
+ <div
4
+ class="flex w-full flex-col items-center gap-2"
5
+ :class="{ 'md:flex-row': layout === 'horizontal' && !form.authenticator }"
6
+ >
7
+ <Input
8
+ v-if="showInput"
9
+ name="url"
10
+ :aria-label="$td('solid.logIn.label', 'Login url')"
11
+ :placeholder="$td('solid.logIn.placeholder', 'https://me.solidcommunity.net')"
12
+ class="w-full md:min-w-96"
13
+ />
14
+ <Select
15
+ v-if="form.authenticator"
16
+ name="authenticator"
17
+ class="w-full max-w-full md:min-w-96"
18
+ label-class="sr-only"
19
+ options-class="md:w-96 w-full max-w-[calc(100vw-4rem)]"
20
+ :label="$td('solid.logIn.switchAuthenticatorLabel', 'Authentication method')"
21
+ :options="authenticatorOptions"
22
+ :render-option="renderAuthenticator"
23
+ />
24
+ <div v-if="showDevLogin && !usingManualUrl" class="flex w-full justify-center gap-2">
25
+ <Button
26
+ variant="secondary"
27
+ class="group gap-0"
28
+ :title="$td('solid.logIn.manualUrl', 'Enter url')"
29
+ @click="usingManualUrl = true"
30
+ >
31
+ <IconLogin class="size-4" />
32
+ <span
33
+ v-measure.css="() => (measured = true)"
34
+ class="overflow-hidden pl-1 whitespace-nowrap"
35
+ :class="{
36
+ 'opacity-0': !measured,
37
+ 'w-0 transition-[width] md:group-hover:w-(--width)': measured,
38
+ }"
39
+ >{{ $td('solid.logIn.manualUrl', 'Enter url') }}</span>
40
+ </Button>
41
+ <Button
42
+ :disabled="noLoading && $solid.loginOngoing"
43
+ submit
44
+ class="whitespace-nowrap"
45
+ @click="form.url = 'dev'"
46
+ >
47
+ {{ $td('solid.logIn.dev', 'Log in to dev server') }}
48
+ </Button>
49
+ </div>
50
+ <Button
51
+ v-else
52
+ :disabled="noLoading && $solid.loginOngoing"
53
+ submit
54
+ :class="renderedButtonClasses"
55
+ >
56
+ {{ $td('solid.logIn.submit', 'Log in') }}
57
+ </Button>
58
+ </div>
59
+ <Link
60
+ v-if="allowLegacyAuthenticator && showInput && !form.authenticator"
61
+ class="text-sm font-normal text-gray-700"
62
+ @click="form.authenticator = 'inrupt'"
63
+ >
64
+ {{ $td('solid.logIn.switchAuthenticator', "Can't log in? Try using a different authentication method") }}
65
+ </Link>
66
+ </Form>
67
+ </template>
68
+
69
+ <script setup lang="ts">
70
+ import IconLogin from '~icons/ic/baseline-log-in';
71
+
72
+ import {
73
+ App,
74
+ Button,
75
+ Form,
76
+ Input,
77
+ Link,
78
+ Select,
79
+ UI,
80
+ classes,
81
+ requiredStringInput,
82
+ stringInput,
83
+ translateWithDefault,
84
+ useForm,
85
+ } from '@aerogel/core';
86
+ import { computed, ref } from 'vue';
87
+ import type { HTMLAttributes } from 'vue';
88
+
89
+ import Solid from '@aerogel/plugin-solid/services/Solid';
90
+ import type { AuthenticatorName } from '@aerogel/plugin-solid/auth';
91
+
92
+ const AUTHENTICATOR_LABELS = {
93
+ inrupt: 'Log in using Inrupt\'s authentication library',
94
+ legacy: 'Log in using the legacy authentication library',
95
+ } satisfies Partial<Record<AuthenticatorName, string>>;
96
+
97
+ const {
98
+ class: rootClasses = '',
99
+ layout = 'horizontal',
100
+ buttonClass,
101
+ noLoading,
102
+ allowLegacyAuthenticator,
103
+ } = defineProps<{
104
+ class?: HTMLAttributes['class'];
105
+ buttonClass?: HTMLAttributes['class'];
106
+ layout?: 'vertical' | 'horizontal';
107
+ noLoading?: boolean;
108
+ allowLegacyAuthenticator?: boolean;
109
+ }>();
110
+ const form = useForm({
111
+ url: requiredStringInput(),
112
+ authenticator: stringInput(),
113
+ });
114
+ const authenticatorOptions = Object.keys(AUTHENTICATOR_LABELS) as (keyof typeof AUTHENTICATOR_LABELS)[];
115
+ const measured = ref(false);
116
+ const usingManualUrl = ref(false);
117
+ const showInput = computed(() => !showDevLogin.value || usingManualUrl.value);
118
+ const showDevLogin = computed(
119
+ () => App.development && (!form.url || form.url === 'dev' || form.url.trim().length === 0),
120
+ );
121
+ const renderedClasses = computed(() => classes('flex flex-col items-center gap-2 w-full md:w-auto', rootClasses));
122
+ const renderedButtonClasses = computed(() =>
123
+ classes('w-full whitespace-nowrap', { 'md:w-auto': layout === 'horizontal' && !form.authenticator }, buttonClass));
124
+
125
+ function renderAuthenticator(option: keyof typeof AUTHENTICATOR_LABELS) {
126
+ return translateWithDefault(
127
+ `solid.logIn.switchAuthenticator${option.charAt(0).toUpperCase() + option.slice(1)}`,
128
+ AUTHENTICATOR_LABELS[option],
129
+ );
130
+ }
131
+
132
+ async function submit() {
133
+ if (noLoading) {
134
+ await Solid.login(form.url, { authenticator: form.authenticator as AuthenticatorName });
135
+
136
+ return;
137
+ }
138
+
139
+ await UI.loading(translateWithDefault('solid.loginOngoing', 'Logging in...'), Solid.login(form.url));
140
+ }
141
+ </script>
@@ -1,4 +1,3 @@
1
- import AGSolidStatus from './AGSolidStatus.vue';
2
- import { IAGSolidStatusLoggedInSlotProps } from './AGSolidStatus';
3
-
4
- export { AGSolidStatus, IAGSolidStatusLoggedInSlotProps };
1
+ export { default as SolidAvatar } from './SolidAvatar.vue';
2
+ export { default as SolidImage } from './SolidImage.vue';
3
+ export { default as SolidLogin } from './SolidLogin.vue';
@@ -0,0 +1,22 @@
1
+ import { JSError } from '@noeldemartin/utils';
2
+ import type { JSErrorOptions } from '@noeldemartin/utils';
3
+
4
+ function getErrorMessage(message: string, description?: string | null): string {
5
+ if (!description) {
6
+ return message;
7
+ }
8
+
9
+ return `${message} (${description})`;
10
+ }
11
+
12
+ export default class AuthenticationFailedError extends JSError {
13
+
14
+ public readonly description?: string | null;
15
+
16
+ constructor(message: string, description?: string | null, options?: JSErrorOptions) {
17
+ super(getErrorMessage(message), options);
18
+
19
+ this.description = description;
20
+ }
21
+
22
+ }
@@ -1,4 +1,3 @@
1
- import AuthenticationCancelledError from '@/errors/AuthenticationCancelledError';
2
- import ContainerAlreadyInUse from '@/errors/ContainerAlreadyInUse';
3
-
4
- export { AuthenticationCancelledError, ContainerAlreadyInUse };
1
+ export { default as AuthenticationCancelledError } from '@aerogel/plugin-solid/errors/AuthenticationCancelledError';
2
+ export { default as AuthenticationFailedError } from '@aerogel/plugin-solid/errors/AuthenticationFailedError';
3
+ export { default as ContainerAlreadyInUse } from '@aerogel/plugin-solid/errors/ContainerAlreadyInUse';
@@ -0,0 +1,11 @@
1
+ import { defineFormValidationRule } from '@aerogel/core';
2
+
3
+ export function defineFormValidationRules(): void {
4
+ defineFormValidationRule<string>('container_url', (value) => {
5
+ if (value.endsWith('/')) {
6
+ return;
7
+ }
8
+
9
+ return 'containerEndingSlashMissing';
10
+ });
11
+ }
package/src/index.css ADDED
@@ -0,0 +1 @@
1
+ @source './';
@@ -0,0 +1,24 @@
1
+ import { createApp } from 'vue';
2
+ import { describe, expect, it } from 'vitest';
3
+ import { Errors, bootServices } from '@aerogel/core';
4
+ import { Metadata } from 'soukai-solid';
5
+ import { requireBootedModel } from 'soukai';
6
+
7
+ import solid from './index';
8
+
9
+ describe('Solid plugin', () => {
10
+
11
+ it('Initializes models', async () => {
12
+ // Arrange
13
+ const app = createApp({});
14
+
15
+ await bootServices(app, { $errors: Errors });
16
+
17
+ // Act
18
+ await solid().install(app, {});
19
+
20
+ // Assert
21
+ expect(requireBootedModel('Metadata')).toEqual(Metadata);
22
+ });
23
+
24
+ });
package/src/index.ts ADDED
@@ -0,0 +1,65 @@
1
+ import { Events, bootServices, registerErrorHandler } from '@aerogel/core';
2
+ import { bootSolidModels } from 'soukai-solid';
3
+ import type { Plugin } from '@aerogel/core';
4
+ import type { SolidStore, SolidUserProfile } from '@noeldemartin/solid-utils';
5
+
6
+ import Solid from '@aerogel/plugin-solid/services/Solid';
7
+ import { DEFAULT_STATE } from '@aerogel/plugin-solid/services/Solid.state';
8
+ import { defineFormValidationRules } from '@aerogel/plugin-solid/forms/validation';
9
+ import {
10
+ authenticators as baseAuthenticators,
11
+ getAuthenticator,
12
+ registerAuthenticators,
13
+ setDefaultAuthenticator,
14
+ } from '@aerogel/plugin-solid/auth';
15
+ import { AuthenticationFailedError } from '@aerogel/plugin-solid/errors';
16
+ import type Authenticator from '@aerogel/plugin-solid/auth/Authenticator';
17
+ import type { AuthenticatorName } from '@aerogel/plugin-solid/auth';
18
+
19
+ const services = { $solid: Solid };
20
+
21
+ export * from './auth';
22
+ export * from './components';
23
+ export * from './errors';
24
+ export * from './services/Solid';
25
+ export { Solid };
26
+
27
+ export interface Options {
28
+ autoReconnect?: boolean;
29
+ authenticators?: Record<string, Authenticator>;
30
+ defaultAuthenticator?: AuthenticatorName;
31
+ onUserProfileLoaded?(user: SolidUserProfile, store: SolidStore): Promise<unknown> | unknown;
32
+ }
33
+
34
+ export type SolidServices = typeof services;
35
+
36
+ export default function solid(options: Options = {}): Plugin {
37
+ return {
38
+ async install(app) {
39
+ bootSolidModels();
40
+ registerAuthenticators({ ...baseAuthenticators, ...(options.authenticators ?? {}) });
41
+ defineFormValidationRules();
42
+ setDefaultAuthenticator(getAuthenticator(options.defaultAuthenticator ?? 'inrupt'));
43
+ registerErrorHandler((error) => {
44
+ if (!(error instanceof AuthenticationFailedError)) {
45
+ return;
46
+ }
47
+ return error.description ? `${error.message} (${error.description})` : error.message;
48
+ });
49
+ if (typeof options.autoReconnect === 'boolean') {
50
+ DEFAULT_STATE.autoReconnect = options.autoReconnect;
51
+ Solid.hasPersistedState() || (Solid.autoReconnect = options.autoReconnect);
52
+ }
53
+ if (options.onUserProfileLoaded) {
54
+ Events.on('solid:user-profile-loaded', ([user, store]) => {
55
+ options.onUserProfileLoaded?.(user, store);
56
+ });
57
+ }
58
+ await bootServices(app, services);
59
+ },
60
+ };
61
+ }
62
+
63
+ declare module '@aerogel/core' {
64
+ interface Services extends SolidServices {}
65
+ }
@@ -2,23 +2,25 @@ import { defineServiceState } from '@aerogel/core';
2
2
  import type { ErrorSource } from '@aerogel/core';
3
3
  import type { Fetch, SolidUserProfile } from '@noeldemartin/solid-utils';
4
4
 
5
- import type { AuthSession } from '@/auth/Authenticator';
6
- import type { AuthenticatorName } from '@/auth';
5
+ import type { AuthSession } from '@aerogel/plugin-solid/auth/Authenticator';
6
+ import type { AuthenticatorName } from '@aerogel/plugin-solid/auth';
7
+
8
+ export const DEFAULT_STATE = { autoReconnect: true };
7
9
 
8
10
  export default defineServiceState({
9
11
  name: 'solid',
10
12
  persist: ['autoReconnect', 'dismissed', 'previousSession', 'profiles', 'staleProfiles'],
11
13
  initialState: {
12
- autoReconnect: false,
14
+ autoReconnect: DEFAULT_STATE.autoReconnect,
13
15
  dismissed: false,
14
16
  ignorePreviousSessionError: false,
15
- loginError: null as ErrorSource | null,
17
+ loginStartupError: null as ErrorSource | null,
16
18
  loginOngoing: false,
17
19
  preferredAuthenticator: null as AuthenticatorName | null,
18
20
  previousSession: null as {
21
+ profile?: SolidUserProfile;
19
22
  authenticator: AuthenticatorName;
20
23
  loginUrl: string;
21
- avatarUrl?: string;
22
24
  error: ErrorSource | null;
23
25
  } | null,
24
26
  profiles: {} as Record<string, SolidUserProfile>,
@@ -34,20 +36,14 @@ export default defineServiceState({
34
36
  return state;
35
37
  },
36
38
  computed: {
37
- loggedIn: (state) => !!state.session,
38
- user: (state) => state.session?.user ?? null,
39
- userAvatarUrl: (state) => state.session?.user?.avatarUrl ?? state.previousSession?.avatarUrl ?? null,
40
- wasLoggedIn: (state) => !!state.previousSession?.loginUrl,
39
+ user: (state) => state.session?.user ?? state.previousSession?.profile ?? null,
41
40
  authenticator: (state) => state.session?.authenticator ?? null,
42
- hasLoggedIn(): boolean {
43
- return this.loggedIn || this.wasLoggedIn;
44
- },
45
41
  fetch(): Fetch {
46
- return this.authenticator?.getAuthenticatedFetch() ?? window.fetch.bind(window);
42
+ return this.authenticator?.getAuthenticatedFetch() ?? globalThis.fetch.bind(globalThis);
47
43
  },
48
- error(state) {
49
- if (state.loginError) {
50
- return state.loginError ?? null;
44
+ error(state): ErrorSource | null {
45
+ if (state.loginStartupError) {
46
+ return state.loginStartupError ?? null;
51
47
  }
52
48
 
53
49
  if (state.ignorePreviousSessionError) {