@aerogel/core 0.0.0-next.926bde19326fe7b6b24b277666936862b64d8295 → 0.0.0-next.97312fd206b83ac5ae520da32b1bb3f12fb55969

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 (108) hide show
  1. package/dist/aerogel-core.cjs.js +1 -1
  2. package/dist/aerogel-core.cjs.js.map +1 -1
  3. package/dist/aerogel-core.d.ts +1436 -232
  4. package/dist/aerogel-core.esm.js +1 -1
  5. package/dist/aerogel-core.esm.js.map +1 -1
  6. package/histoire.config.ts +7 -0
  7. package/noeldemartin.config.js +4 -1
  8. package/package.json +14 -4
  9. package/postcss.config.js +6 -0
  10. package/src/assets/histoire.css +3 -0
  11. package/src/bootstrap/bootstrap.test.ts +4 -3
  12. package/src/bootstrap/index.ts +26 -4
  13. package/src/bootstrap/options.ts +3 -0
  14. package/src/components/AGAppLayout.vue +7 -2
  15. package/src/components/AGAppModals.vue +15 -0
  16. package/src/components/AGAppOverlays.vue +10 -8
  17. package/src/components/AGAppSnackbars.vue +13 -0
  18. package/src/components/constants.ts +8 -0
  19. package/src/components/forms/AGButton.vue +33 -10
  20. package/src/components/forms/AGCheckbox.vue +41 -0
  21. package/src/components/forms/AGInput.vue +15 -9
  22. package/src/components/forms/AGSelect.story.vue +46 -0
  23. package/src/components/forms/AGSelect.vue +60 -0
  24. package/src/components/forms/index.ts +5 -5
  25. package/src/components/headless/forms/AGHeadlessButton.vue +7 -7
  26. package/src/components/headless/forms/AGHeadlessInput.ts +23 -3
  27. package/src/components/headless/forms/AGHeadlessInput.vue +11 -8
  28. package/src/components/headless/forms/AGHeadlessInputError.vue +1 -1
  29. package/src/components/headless/forms/AGHeadlessInputInput.vue +15 -3
  30. package/src/components/headless/forms/AGHeadlessInputLabel.vue +8 -2
  31. package/src/components/headless/forms/AGHeadlessSelect.ts +42 -0
  32. package/src/components/headless/forms/AGHeadlessSelect.vue +77 -0
  33. package/src/components/headless/forms/AGHeadlessSelectButton.vue +24 -0
  34. package/src/components/headless/forms/AGHeadlessSelectError.vue +26 -0
  35. package/src/components/headless/forms/AGHeadlessSelectLabel.vue +24 -0
  36. package/src/components/headless/forms/AGHeadlessSelectOption.ts +4 -0
  37. package/src/components/headless/forms/AGHeadlessSelectOption.vue +39 -0
  38. package/src/components/headless/forms/AGHeadlessSelectOptions.ts +3 -0
  39. package/src/components/headless/forms/index.ts +9 -1
  40. package/src/components/headless/index.ts +1 -0
  41. package/src/components/headless/modals/AGHeadlessModal.ts +27 -0
  42. package/src/components/headless/modals/AGHeadlessModal.vue +3 -5
  43. package/src/components/headless/modals/AGHeadlessModalPanel.vue +5 -1
  44. package/src/components/headless/modals/index.ts +4 -6
  45. package/src/components/headless/snackbars/AGHeadlessSnackbar.vue +10 -0
  46. package/src/components/headless/snackbars/index.ts +40 -0
  47. package/src/components/index.ts +3 -1
  48. package/src/components/lib/AGErrorMessage.vue +16 -0
  49. package/src/components/lib/AGLink.vue +9 -0
  50. package/src/components/lib/AGMarkdown.vue +36 -0
  51. package/src/components/lib/AGMeasured.vue +15 -0
  52. package/src/components/lib/AGStartupCrash.vue +31 -0
  53. package/src/components/lib/index.ts +5 -0
  54. package/src/components/modals/AGAlertModal.ts +15 -0
  55. package/src/components/modals/AGAlertModal.vue +4 -16
  56. package/src/components/modals/AGConfirmModal.ts +27 -0
  57. package/src/components/modals/AGConfirmModal.vue +8 -12
  58. package/src/components/modals/AGErrorReportModal.ts +46 -0
  59. package/src/components/modals/AGErrorReportModal.vue +54 -0
  60. package/src/components/modals/AGErrorReportModalButtons.vue +111 -0
  61. package/src/components/modals/AGErrorReportModalTitle.vue +25 -0
  62. package/src/components/modals/AGLoadingModal.ts +23 -0
  63. package/src/components/modals/AGLoadingModal.vue +15 -0
  64. package/src/components/modals/AGModal.ts +1 -1
  65. package/src/components/modals/AGModal.vue +26 -5
  66. package/src/components/modals/AGModalTitle.vue +9 -0
  67. package/src/components/modals/AGPromptModal.ts +30 -0
  68. package/src/components/modals/AGPromptModal.vue +34 -0
  69. package/src/components/modals/index.ts +16 -6
  70. package/src/components/snackbars/AGSnackbar.vue +36 -0
  71. package/src/components/snackbars/index.ts +3 -0
  72. package/src/components/utils.ts +10 -0
  73. package/src/directives/index.ts +18 -3
  74. package/src/directives/measure.ts +12 -0
  75. package/src/errors/Errors.state.ts +31 -0
  76. package/src/errors/Errors.ts +188 -0
  77. package/src/errors/index.ts +55 -0
  78. package/src/forms/Form.ts +27 -15
  79. package/src/forms/utils.ts +17 -0
  80. package/src/lang/Lang.ts +12 -4
  81. package/src/lang/index.ts +3 -5
  82. package/src/lang/utils.ts +4 -0
  83. package/src/main.histoire.ts +1 -0
  84. package/src/main.ts +1 -2
  85. package/src/plugins/Plugin.ts +1 -0
  86. package/src/plugins/index.ts +19 -0
  87. package/src/services/App.state.ts +9 -2
  88. package/src/services/App.ts +36 -2
  89. package/src/services/Service.ts +146 -49
  90. package/src/services/index.ts +21 -4
  91. package/src/services/store.ts +27 -0
  92. package/src/ui/UI.state.ts +11 -1
  93. package/src/ui/UI.ts +164 -17
  94. package/src/ui/index.ts +15 -4
  95. package/src/utils/composition/events.ts +1 -0
  96. package/src/utils/composition/forms.ts +11 -0
  97. package/src/utils/index.ts +2 -0
  98. package/src/utils/markdown.ts +11 -2
  99. package/src/utils/tailwindcss.test.ts +26 -0
  100. package/src/utils/tailwindcss.ts +7 -0
  101. package/src/utils/vue.ts +15 -4
  102. package/tailwind.config.js +4 -0
  103. package/tsconfig.json +1 -0
  104. package/vite.config.ts +2 -1
  105. package/.eslintrc.js +0 -3
  106. package/src/components/basic/AGMarkdown.vue +0 -35
  107. package/src/components/basic/index.ts +0 -3
  108. package/src/globals.ts +0 -6
@@ -0,0 +1,188 @@
1
+ import { JSError, facade, isObject, objectWithoutEmpty, toString } from '@noeldemartin/utils';
2
+
3
+ import App from '@/services/App';
4
+ import ServiceBootError from '@/errors/ServiceBootError';
5
+ import UI, { UIComponents } from '@/ui/UI';
6
+ import { translateWithDefault } from '@/lang/utils';
7
+
8
+ import Service from './Errors.state';
9
+ import { Colors } from '@/components/constants';
10
+ import type { AGErrorReportModalProps } from '@/components/modals/AGErrorReportModal';
11
+ import type { ErrorReport, ErrorReportLog, ErrorSource } from './Errors.state';
12
+ import type { ModalComponent } from '@/ui/UI.state';
13
+
14
+ export class ErrorsService extends Service {
15
+
16
+ public forceReporting: boolean = false;
17
+ private enabled: boolean = true;
18
+
19
+ public enable(): void {
20
+ this.enabled = true;
21
+ }
22
+
23
+ public disable(): void {
24
+ this.enabled = false;
25
+ }
26
+
27
+ public async inspect(error: ErrorSource | ErrorReport[]): Promise<void> {
28
+ const reports = Array.isArray(error) ? (error as ErrorReport[]) : [await this.createErrorReport(error)];
29
+
30
+ if (reports.length === 0) {
31
+ UI.alert(translateWithDefault('errors.inspectEmpty', 'Nothing to inspect!'));
32
+
33
+ return;
34
+ }
35
+
36
+ UI.openModal<ModalComponent<AGErrorReportModalProps>>(UI.requireComponent(UIComponents.ErrorReportModal), {
37
+ reports,
38
+ });
39
+ }
40
+
41
+ public async report(error: ErrorSource, message?: string): Promise<void> {
42
+ if (App.development || App.testing) {
43
+ this.logError(error);
44
+ }
45
+
46
+ if (!this.enabled) {
47
+ throw error;
48
+ }
49
+
50
+ if (!App.isMounted()) {
51
+ const startupError = await this.createStartupErrorReport(error);
52
+
53
+ if (startupError) {
54
+ this.setState({ startupErrors: this.startupErrors.concat(startupError) });
55
+ }
56
+
57
+ return;
58
+ }
59
+
60
+ const report = await this.createErrorReport(error);
61
+ const log: ErrorReportLog = {
62
+ report,
63
+ seen: false,
64
+ date: new Date(),
65
+ };
66
+
67
+ UI.showSnackbar(
68
+ message ??
69
+ translateWithDefault('errors.notice', 'Something went wrong, but it\'s not your fault. Try again!'),
70
+ {
71
+ color: Colors.Danger,
72
+ actions: [
73
+ {
74
+ text: translateWithDefault('errors.viewDetails', 'View details'),
75
+ dismiss: true,
76
+ handler: () =>
77
+ UI.openModal<ModalComponent<AGErrorReportModalProps>>(
78
+ UI.requireComponent(UIComponents.ErrorReportModal),
79
+ { reports: [report] },
80
+ ),
81
+ },
82
+ ],
83
+ },
84
+ );
85
+
86
+ this.setState({ logs: [log].concat(this.logs) });
87
+ }
88
+
89
+ public see(report: ErrorReport): void {
90
+ this.setState({
91
+ logs: this.logs.map((log) => {
92
+ if (log.report !== report) {
93
+ return log;
94
+ }
95
+
96
+ return {
97
+ ...log,
98
+ seen: true,
99
+ };
100
+ }),
101
+ });
102
+ }
103
+
104
+ public seeAll(): void {
105
+ this.setState({
106
+ logs: this.logs.map((log) => ({
107
+ ...log,
108
+ seen: true,
109
+ })),
110
+ });
111
+ }
112
+
113
+ public getErrorMessage(error: ErrorSource): string {
114
+ if (typeof error === 'string') {
115
+ return error;
116
+ }
117
+
118
+ if (error instanceof Error || error instanceof JSError) {
119
+ return error.message;
120
+ }
121
+
122
+ if (isObject(error)) {
123
+ return toString(error['message'] ?? error['description'] ?? 'Unknown error object');
124
+ }
125
+
126
+ return translateWithDefault('errors.unknown', 'Unknown Error');
127
+ }
128
+
129
+ private logError(error: unknown): void {
130
+ // eslint-disable-next-line no-console
131
+ console.error(error);
132
+
133
+ if (isObject(error) && error.cause) {
134
+ this.logError(error.cause);
135
+ }
136
+ }
137
+
138
+ private async createErrorReport(error: ErrorSource): Promise<ErrorReport> {
139
+ if (typeof error === 'string') {
140
+ return { title: error };
141
+ }
142
+
143
+ if (error instanceof Error || error instanceof JSError) {
144
+ return this.createErrorReportFromError(error);
145
+ }
146
+
147
+ if (isObject(error)) {
148
+ return objectWithoutEmpty({
149
+ title: toString(
150
+ error['name'] ?? error['title'] ?? translateWithDefault('errors.unknown', 'Unknown Error'),
151
+ ),
152
+ description: toString(
153
+ error['message'] ??
154
+ error['description'] ??
155
+ translateWithDefault('errors.unknownDescription', 'Unknown error object'),
156
+ ),
157
+ error,
158
+ });
159
+ }
160
+
161
+ return {
162
+ title: translateWithDefault('errors.unknown', 'Unknown Error'),
163
+ error,
164
+ };
165
+ }
166
+
167
+ private async createStartupErrorReport(error: ErrorSource): Promise<ErrorReport | null> {
168
+ if (error instanceof ServiceBootError) {
169
+ // Ignore second-order boot errors in order to have a cleaner startup crash screen.
170
+ return error.cause instanceof ServiceBootError ? null : this.createErrorReport(error.cause);
171
+ }
172
+
173
+ return this.createErrorReport(error);
174
+ }
175
+
176
+ private createErrorReportFromError(error: Error | JSError, defaults: Partial<ErrorReport> = {}): ErrorReport {
177
+ return {
178
+ title: error.name,
179
+ description: error.message,
180
+ details: error.stack,
181
+ error,
182
+ ...defaults,
183
+ };
184
+ }
185
+
186
+ }
187
+
188
+ export default facade(new ErrorsService());
@@ -0,0 +1,55 @@
1
+ import type { App } from 'vue';
2
+
3
+ import { bootServices } from '@/services';
4
+ import { definePlugin } from '@/plugins';
5
+
6
+ import Errors from './Errors';
7
+ import { ErrorReport, ErrorReportLog, ErrorSource } from './Errors.state';
8
+
9
+ export { Errors, ErrorSource, ErrorReport, ErrorReportLog };
10
+
11
+ const services = { $errors: Errors };
12
+ const frameworkHandler: ErrorHandler = (error) => {
13
+ if (!Errors.instance) {
14
+ // eslint-disable-next-line no-console
15
+ console.warn('Errors service hasn\'t been initialized properly!');
16
+
17
+ // eslint-disable-next-line no-console
18
+ console.error(error);
19
+
20
+ return true;
21
+ }
22
+
23
+ Errors.report(error);
24
+
25
+ return true;
26
+ };
27
+
28
+ function setUpErrorHandler(app: App, baseHandler: ErrorHandler = () => false): void {
29
+ const errorHandler: ErrorHandler = (error) => baseHandler(error) || frameworkHandler(error);
30
+
31
+ app.config.errorHandler = errorHandler;
32
+ globalThis.onerror = (event, _, __, ___, error) => errorHandler(error ?? event);
33
+ globalThis.onunhandledrejection = (event) => errorHandler(event.reason);
34
+ }
35
+
36
+ export type ErrorHandler = (error: ErrorSource) => boolean;
37
+ export type ErrorsServices = typeof services;
38
+
39
+ export default definePlugin({
40
+ async install(app, options) {
41
+ setUpErrorHandler(app, options.handleError);
42
+
43
+ await bootServices(app, services);
44
+ },
45
+ });
46
+
47
+ declare module '@/bootstrap/options' {
48
+ export interface AerogelOptions {
49
+ handleError?(error: ErrorSource): boolean;
50
+ }
51
+ }
52
+
53
+ declare module '@/services' {
54
+ export interface Services extends ErrorsServices {}
55
+ }
package/src/forms/Form.ts CHANGED
@@ -6,6 +6,8 @@ import type { ComputedRef, DeepReadonly, Ref, UnwrapNestedRefs } from 'vue';
6
6
  export const FormFieldTypes = {
7
7
  String: 'string',
8
8
  Number: 'number',
9
+ Boolean: 'boolean',
10
+ Object: 'object',
9
11
  } as const;
10
12
 
11
13
  export interface FormFieldDefinition<TType extends FormFieldType = FormFieldType, TRules extends string = string> {
@@ -16,6 +18,7 @@ export interface FormFieldDefinition<TType extends FormFieldType = FormFieldType
16
18
 
17
19
  export type FormFieldDefinitions = Record<string, FormFieldDefinition>;
18
20
  export type FormFieldType = ObjectValues<typeof FormFieldTypes>;
21
+ export type FormFieldValue = GetFormFieldValue<FormFieldType>;
19
22
 
20
23
  export type FormData<T> = {
21
24
  -readonly [k in keyof T]: T[k] extends FormFieldDefinition<infer TType, infer TRules>
@@ -33,15 +36,20 @@ export type GetFormFieldValue<TType> = TType extends typeof FormFieldTypes.Strin
33
36
  ? string
34
37
  : TType extends typeof FormFieldTypes.Number
35
38
  ? number
39
+ : TType extends typeof FormFieldTypes.Boolean
40
+ ? boolean
41
+ : TType extends typeof FormFieldTypes.Object
42
+ ? object
36
43
  : never;
37
44
 
45
+ const validForms: WeakMap<Form, ComputedRef<boolean>> = new WeakMap();
46
+
38
47
  export default class Form<Fields extends FormFieldDefinitions = FormFieldDefinitions> extends MagicObject {
39
48
 
40
49
  public errors: DeepReadonly<UnwrapNestedRefs<FormErrors<Fields>>>;
41
50
 
42
51
  private _fields: Fields;
43
52
  private _data: FormData<Fields>;
44
- private _valid: ComputedRef<boolean>;
45
53
  private _submitted: Ref<boolean>;
46
54
  private _errors: FormErrors<Fields>;
47
55
 
@@ -52,13 +60,17 @@ export default class Form<Fields extends FormFieldDefinitions = FormFieldDefinit
52
60
  this._submitted = ref(false);
53
61
  this._data = this.getInitialData(fields);
54
62
  this._errors = this.getInitialErrors(fields);
55
- this._valid = computed(() => !Object.values(this._errors).some((error) => error !== null));
63
+
64
+ validForms.set(
65
+ this,
66
+ computed(() => !Object.values(this._errors).some((error) => error !== null)),
67
+ );
56
68
 
57
69
  this.errors = readonly(this._errors);
58
70
  }
59
71
 
60
72
  public get valid(): boolean {
61
- return this._valid.value;
73
+ return !!validForms.get(this)?.value;
62
74
  }
63
75
 
64
76
  public get submitted(): boolean {
@@ -78,10 +90,10 @@ export default class Form<Fields extends FormFieldDefinitions = FormFieldDefinit
78
90
  }
79
91
 
80
92
  public validate(): boolean {
81
- const errors = Object.entries(this._fields).reduce((errors, [name, definition]) => {
82
- errors[name] = this.getFieldErrors(name, definition);
93
+ const errors = Object.entries(this._fields).reduce((formErrors, [name, definition]) => {
94
+ formErrors[name] = this.getFieldErrors(name, definition);
83
95
 
84
- return errors;
96
+ return formErrors;
85
97
  }, {} as Record<string, string[] | null>);
86
98
 
87
99
  this.resetErrors(errors);
@@ -89,11 +101,11 @@ export default class Form<Fields extends FormFieldDefinitions = FormFieldDefinit
89
101
  return this.valid;
90
102
  }
91
103
 
92
- public reset(): void {
104
+ public reset(options: { keepData?: boolean; keepErrors?: boolean } = {}): void {
93
105
  this._submitted.value = false;
94
106
 
95
- this.resetData();
96
- this.resetErrors();
107
+ options.keepData || this.resetData();
108
+ options.keepErrors || this.resetErrors();
97
109
  }
98
110
 
99
111
  public submit(): boolean {
@@ -135,10 +147,10 @@ export default class Form<Fields extends FormFieldDefinitions = FormFieldDefinit
135
147
  return {} as FormData<Fields>;
136
148
  }
137
149
 
138
- const data = Object.entries(fields).reduce((data, [name, definition]) => {
139
- data[name as keyof Fields] = (definition.default ?? null) as FormData<Fields>[keyof Fields];
150
+ const data = Object.entries(fields).reduce((formData, [name, definition]) => {
151
+ formData[name as keyof Fields] = (definition.default ?? null) as FormData<Fields>[keyof Fields];
140
152
 
141
- return data;
153
+ return formData;
142
154
  }, {} as FormData<Fields>);
143
155
 
144
156
  return reactive(data) as FormData<Fields>;
@@ -149,10 +161,10 @@ export default class Form<Fields extends FormFieldDefinitions = FormFieldDefinit
149
161
  return {} as FormErrors<Fields>;
150
162
  }
151
163
 
152
- const errors = Object.keys(fields).reduce((errors, name) => {
153
- errors[name as keyof Fields] = null;
164
+ const errors = Object.keys(fields).reduce((formErrors, name) => {
165
+ formErrors[name as keyof Fields] = null;
154
166
 
155
- return errors;
167
+ return formErrors;
156
168
  }, {} as FormErrors<Fields>);
157
169
 
158
170
  return reactive(errors) as FormErrors<Fields>;
@@ -1,6 +1,23 @@
1
1
  import { FormFieldTypes } from './Form';
2
2
  import type { FormFieldDefinition } from './Form';
3
3
 
4
+ export function booleanInput(defaultValue?: boolean): FormFieldDefinition<typeof FormFieldTypes.Boolean> {
5
+ return {
6
+ default: defaultValue,
7
+ type: FormFieldTypes.Boolean,
8
+ };
9
+ }
10
+
11
+ export function requiredBooleanInput(
12
+ defaultValue?: boolean,
13
+ ): FormFieldDefinition<typeof FormFieldTypes.Boolean, 'required'> {
14
+ return {
15
+ default: defaultValue,
16
+ type: FormFieldTypes.Boolean,
17
+ rules: 'required',
18
+ };
19
+ }
20
+
4
21
  export function requiredNumberInput(
5
22
  defaultValue?: number,
6
23
  ): FormFieldDefinition<typeof FormFieldTypes.Number, 'required'> {
package/src/lang/Lang.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { facade } from '@noeldemartin/utils';
1
+ import { facade, toString } from '@noeldemartin/utils';
2
2
 
3
3
  import App from '@/services/App';
4
4
  import Service from '@/services/Service';
@@ -17,7 +17,7 @@ export class LangService extends Service {
17
17
  this.provider = {
18
18
  translate: (key) => {
19
19
  // eslint-disable-next-line no-console
20
- App.isDevelopment && console.warn('Lang provider is missing');
20
+ App.development && console.warn('Lang provider is missing');
21
21
 
22
22
  return key;
23
23
  },
@@ -41,10 +41,18 @@ export class LangService extends Service {
41
41
  ): string {
42
42
  defaultMessage ??= defaultMessageOrParameters as string;
43
43
 
44
- const parameters = typeof defaultMessageOrParameters === 'string' ? {} : defaultMessageOrParameters;
44
+ const parameters = typeof defaultMessageOrParameters === 'string' ? {} : defaultMessageOrParameters ?? {};
45
45
  const message = this.provider.translate(key, parameters) ?? key;
46
46
 
47
- return message === key ? defaultMessage : message;
47
+ if (message === key) {
48
+ return Object.entries(parameters).reduce(
49
+ (renderedMessage, [name, value]) =>
50
+ renderedMessage.replace(new RegExp(`\\{\\s*${name}\\s*\\}`, 'g'), toString(value)),
51
+ defaultMessage,
52
+ );
53
+ }
54
+
55
+ return message;
48
56
  }
49
57
 
50
58
  }
package/src/lang/index.ts CHANGED
@@ -2,16 +2,14 @@ import { bootServices } from '@/services';
2
2
  import { definePlugin } from '@/plugins';
3
3
 
4
4
  import Lang, { LangProvider } from './Lang';
5
+ import { translate, translateWithDefault } from './utils';
5
6
 
6
- export { Lang, LangProvider };
7
+ export { Lang, LangProvider, translate, translateWithDefault };
7
8
 
8
9
  const services = { $lang: Lang };
9
10
 
10
11
  export type LangServices = typeof services;
11
12
 
12
- export const translate = Lang.translate.bind(Lang);
13
- export const translateWithDefault = Lang.translateWithDefault.bind(Lang);
14
-
15
13
  export default definePlugin({
16
14
  async install(app) {
17
15
  app.config.globalProperties.$t ??= translate;
@@ -22,7 +20,7 @@ export default definePlugin({
22
20
  });
23
21
 
24
22
  declare module '@/services' {
25
- interface Services extends LangServices {}
23
+ export interface Services extends LangServices {}
26
24
  }
27
25
 
28
26
  declare module '@vue/runtime-core' {
@@ -0,0 +1,4 @@
1
+ import Lang from './Lang';
2
+
3
+ export const translate = Lang.translate.bind(Lang);
4
+ export const translateWithDefault = Lang.translateWithDefault.bind(Lang);
@@ -0,0 +1 @@
1
+ import './assets/histoire.css';
package/src/main.ts CHANGED
@@ -1,7 +1,6 @@
1
- import './globals';
2
-
3
1
  export * from './bootstrap';
4
2
  export * from './components';
3
+ export * from './errors';
5
4
  export * from './forms';
6
5
  export * from './lang';
7
6
  export * from './plugins';
@@ -3,5 +3,6 @@ import type { App } from 'vue';
3
3
  import type { AerogelOptions } from '@/bootstrap/options';
4
4
 
5
5
  export interface Plugin {
6
+ name?: string;
6
7
  install(app: App, options: AerogelOptions): void | Promise<void>;
7
8
  }
@@ -1,3 +1,7 @@
1
+ import type { GetClosureArgs } from '@noeldemartin/utils';
2
+
3
+ import App from '@/services/App';
4
+
1
5
  import type { Plugin } from './Plugin';
2
6
 
3
7
  export * from './Plugin';
@@ -5,3 +9,18 @@ export * from './Plugin';
5
9
  export function definePlugin<T extends Plugin>(plugin: T): T {
6
10
  return plugin;
7
11
  }
12
+
13
+ export async function installPlugins(plugins: Plugin[], ...args: GetClosureArgs<Plugin['install']>): Promise<void> {
14
+ App.setState(
15
+ 'plugins',
16
+ plugins.reduce((pluginsMap, plugin) => {
17
+ if (plugin.name) {
18
+ pluginsMap[plugin.name] = plugin;
19
+ }
20
+
21
+ return pluginsMap;
22
+ }, {} as Record<string, Plugin>),
23
+ );
24
+
25
+ await Promise.all(plugins.map((plugin) => plugin.install(...args)) ?? []);
26
+ }
@@ -1,10 +1,17 @@
1
+ import Aerogel from 'virtual:aerogel';
2
+
1
3
  import { defineServiceState } from '@/services/Service';
4
+ import type { Plugin } from '@/plugins/Plugin';
2
5
 
3
6
  export default defineServiceState({
7
+ name: 'app',
4
8
  initialState: {
5
- environment: __AG_ENV,
9
+ plugins: {} as Record<string, Plugin>,
10
+ environment: Aerogel.environment,
11
+ sourceUrl: Aerogel.sourceUrl,
6
12
  },
7
13
  computed: {
8
- isDevelopment: (state) => state.environment === 'development',
14
+ development: (state) => state.environment === 'development',
15
+ testing: (state) => state.environment === 'testing',
9
16
  },
10
17
  });
@@ -1,7 +1,41 @@
1
- import { facade } from '@noeldemartin/utils';
1
+ import { PromisedValue, facade, forever, updateLocationQueryParameters } from '@noeldemartin/utils';
2
+
3
+ import Events from '@/services/Events';
4
+ import type { Plugin } from '@/plugins';
2
5
 
3
6
  import Service from './App.state';
4
7
 
5
- export class AppService extends Service {}
8
+ export class AppService extends Service {
9
+
10
+ public readonly ready = new PromisedValue<void>();
11
+ public readonly mounted = new PromisedValue<void>();
12
+
13
+ public isReady(): boolean {
14
+ return this.ready.isResolved();
15
+ }
16
+
17
+ public isMounted(): boolean {
18
+ return this.mounted.isResolved();
19
+ }
20
+
21
+ public async reload(queryParameters?: Record<string, string | undefined>): Promise<void> {
22
+ queryParameters && updateLocationQueryParameters(queryParameters);
23
+
24
+ location.reload();
25
+
26
+ // Stall until the reload happens
27
+ await forever();
28
+ }
29
+
30
+ public plugin<T extends Plugin = Plugin>(name: string): T | null {
31
+ return (this.plugins[name] as T) ?? null;
32
+ }
33
+
34
+ protected async boot(): Promise<void> {
35
+ Events.once('application-ready', () => this.ready.resolve());
36
+ Events.once('application-mounted', () => this.mounted.resolve());
37
+ }
38
+
39
+ }
6
40
 
7
41
  export default facade(new AppService());