@lilaquadrat/design-core 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 (86) hide show
  1. package/README.md +236 -0
  2. package/html/index.html +25 -0
  3. package/html/index.mail.html +15 -0
  4. package/html/index.server.html +29 -0
  5. package/package.json +148 -0
  6. package/src/client-entry.ts +28 -0
  7. package/src/components/partials/action.partial.vue +30 -0
  8. package/src/components/partials/client-only.partial.vue +11 -0
  9. package/src/components/partials/error.partial.vue +73 -0
  10. package/src/components/partials/main-components.partial.vue +167 -0
  11. package/src/components/partials/mediadetection.partial.vue +71 -0
  12. package/src/components/partials/qrcode.partial.vue +35 -0
  13. package/src/customs.ts +12 -0
  14. package/src/env.d.ts +1 -0
  15. package/src/functions/PaymenyProviderFactory.ts +27 -0
  16. package/src/functions/lila-dom.ts +100 -0
  17. package/src/functions/shopify.provider.ts +378 -0
  18. package/src/functions/stripe.provider.ts +181 -0
  19. package/src/globals.d.ts +19 -0
  20. package/src/index.ts +47 -0
  21. package/src/interfaces/AppState.interface.ts +4 -0
  22. package/src/interfaces/EditorConfiguration.interface.ts +15 -0
  23. package/src/interfaces/EventDeclaration.interface.ts +19 -0
  24. package/src/interfaces/FrontendConfig.interface.ts +42 -0
  25. package/src/interfaces/GenericEvents.interface.ts +6 -0
  26. package/src/interfaces/GenericState.interface.ts +5 -0
  27. package/src/interfaces/IconsPartial.ts +9 -0
  28. package/src/interfaces/IdTokenExtended.interface.ts +7 -0
  29. package/src/interfaces/ModuleBaseProps.interface.ts +8 -0
  30. package/src/interfaces/PaymentProvider.interface.ts +13 -0
  31. package/src/libs/ActionNotice.ts +235 -0
  32. package/src/libs/Models.class.ts +482 -0
  33. package/src/main.ts +84 -0
  34. package/src/mixins/createCookieString.ts +78 -0
  35. package/src/mixins/createModelDeclaration.ts +29 -0
  36. package/src/mixins/createRouter.ts +30 -0
  37. package/src/mixins/date.ts +23 -0
  38. package/src/mixins/formatSize.ts +12 -0
  39. package/src/mixins/getAnchor.ts +14 -0
  40. package/src/mixins/getRoutes.ts +50 -0
  41. package/src/mixins/hasSlotContent.ts +32 -0
  42. package/src/mixins/hooks.ts +104 -0
  43. package/src/mixins/loadComponents.ts +107 -0
  44. package/src/mixins/logger.ts +32 -0
  45. package/src/mixins/replaceVariables.ts +19 -0
  46. package/src/mixins/scroll.ts +83 -0
  47. package/src/models/Address.model.ts +24 -0
  48. package/src/models/Contact.model.ts +22 -0
  49. package/src/models.ts +2 -0
  50. package/src/plugins/auth.ts +121 -0
  51. package/src/plugins/currency.ts +26 -0
  52. package/src/plugins/events.ts +156 -0
  53. package/src/plugins/filters.ts +43 -0
  54. package/src/plugins/inview.ts +351 -0
  55. package/src/plugins/replacer.ts +18 -0
  56. package/src/plugins/resize.ts +128 -0
  57. package/src/plugins/signupFlow.ts +89 -0
  58. package/src/plugins/traceable.ts +53 -0
  59. package/src/plugins/translations.ts +29 -0
  60. package/src/plugins/youtube.ts +80 -0
  61. package/src/routes.ts +132 -0
  62. package/src/server-entry.ts +113 -0
  63. package/src/stores/calls.store.ts +33 -0
  64. package/src/stores/cart.store.ts +186 -0
  65. package/src/stores/content.store.ts +83 -0
  66. package/src/stores/editor.store.ts +27 -0
  67. package/src/stores/files.store.ts +166 -0
  68. package/src/stores/main.store.ts +184 -0
  69. package/src/stores/user.store.ts +124 -0
  70. package/src/translations/de.ts +138 -0
  71. package/src/views/content.view.vue +219 -0
  72. package/src/views/download.view.vue +143 -0
  73. package/src/views/editor.view.vue +243 -0
  74. package/src/views/login.view.vue +82 -0
  75. package/src/views/signup-account.view.vue +64 -0
  76. package/tooling/cypress/config.ts +80 -0
  77. package/tooling/cypress/generate-manifest.js +5 -0
  78. package/tooling/cypress/manifest-utils.mjs +40 -0
  79. package/tooling/cypress/run-parallel.mjs +77 -0
  80. package/tooling/cypress/support/commands.ts +436 -0
  81. package/tooling/cypress/support/e2e.ts +17 -0
  82. package/tooling/eslint.config.js +223 -0
  83. package/tooling/preview.plugin.ts +134 -0
  84. package/tooling/ssr-test/index.mjs +391 -0
  85. package/tooling/stylelint.config.mjs +24 -0
  86. package/tooling/vite.ts +246 -0
@@ -0,0 +1,134 @@
1
+ import type { Plugin, UserConfig } from 'vite';
2
+
3
+ const OUTPUT_DIR = 'dist/preview';
4
+ const PREVIEW_ASSET_DIR = 'previewAssets';
5
+
6
+ function escapeRegExp (input: string) {
7
+
8
+ return input.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
9
+
10
+ }
11
+
12
+ /**
13
+ * drops the gallery fixture images from every build that is not the preview.
14
+ *
15
+ * the viewData fixtures import them statically, and vite emits an imported asset
16
+ * while transforming - so even though rolldown tree shakes preview.view.vue out
17
+ * of the app bundle, the images survive as orphans nobody references.
18
+ */
19
+ export function dropPreviewAssets (): Plugin {
20
+
21
+ return {
22
+ name : 'drop-preview-assets',
23
+ enforce: 'post',
24
+ apply : 'build',
25
+ generateBundle (_options, bundle: Record<string, any>) {
26
+
27
+ Object.keys(bundle).forEach((fileName) => {
28
+
29
+ const single = bundle[fileName];
30
+
31
+ if(single.type !== 'asset') return;
32
+
33
+ const origins: string[] = [single.originalFileName, ...single.originalFileNames ?? []].filter(Boolean);
34
+
35
+ if(origins.some((origin) => origin.includes(PREVIEW_ASSET_DIR))) delete bundle[fileName];
36
+
37
+ });
38
+
39
+ }
40
+ };
41
+
42
+ }
43
+
44
+ /**
45
+ * build overrides for the shareable static gallery (yarn build:preview).
46
+ *
47
+ * everything here exists because the output is opened straight from disk, where
48
+ * each file is its own opaque origin.
49
+ */
50
+ export function previewBuildOptions (base: UserConfig['build']): UserConfig['build'] {
51
+
52
+ return {
53
+ ...base,
54
+ /**
55
+ * inline fonts and images as data URIs: browsers block the font fetches a
56
+ * separate .woff would need
57
+ */
58
+ assetsInlineLimit: 512 * 1024,
59
+ rolldownOptions : {
60
+ ...base?.rolldownOptions,
61
+ output: {
62
+ ...base?.rolldownOptions?.output,
63
+ // a single classic script: file:// blocks `type="module"` as cross origin
64
+ dir : OUTPUT_DIR,
65
+ format : 'iife',
66
+ codeSplitting : false,
67
+ entryFileNames: '[name].js',
68
+ }
69
+ }
70
+ };
71
+
72
+ }
73
+
74
+ /**
75
+ * folds the script and the stylesheet into index.html, so the preview is a self
76
+ * contained file that opens over file:// with no server.
77
+ */
78
+ export default function previewBundle (): Plugin {
79
+
80
+ return {
81
+ name : 'preview-bundle',
82
+ enforce: 'post',
83
+ apply : 'build',
84
+ generateBundle (_options, bundle: Record<string, any>) {
85
+
86
+ const html = bundle['index.html'];
87
+
88
+ if(!html) return;
89
+
90
+ let source = html.source.toString();
91
+ let script = '';
92
+
93
+ Object.keys(bundle).forEach((fileName) => {
94
+
95
+ const single = bundle[fileName];
96
+
97
+ if(fileName === 'index.html') return;
98
+
99
+ if(single.type === 'chunk') {
100
+
101
+ // a literal </script> inside the code would end the tag early
102
+ script += single.code.replace(/<\/script>/gi, '<\\/script>');
103
+ source = source.replace(new RegExp(`<script[^>]*src="[^"]*${escapeRegExp(fileName)}"[^>]*></script>\\s*`), '');
104
+ delete bundle[fileName];
105
+ return;
106
+
107
+ }
108
+
109
+ if(fileName.endsWith('.css')) {
110
+
111
+ // replacer functions only: a replacement string would expand $& and
112
+ // $\` in the asset as substitution patterns
113
+ source = source.replace(new RegExp(`<link[^>]*href="[^"]*${escapeRegExp(fileName)}"[^>]*>`), () => `<style>${single.source}</style>`);
114
+ delete bundle[fileName];
115
+
116
+ }
117
+
118
+ });
119
+
120
+ source = source.replace(/<link[^>]*rel="modulepreload"[^>]*>\s*/g, '');
121
+ // the manifest is the one head asset a file:// origin cannot fetch
122
+ source = source.replace(/<link[^>]*rel="manifest"[^>]*>\s*/g, '');
123
+ Object.keys(bundle).filter((fileName) => fileName.endsWith('.webmanifest')).forEach((fileName) => delete bundle[fileName]);
124
+
125
+ /**
126
+ * at the end of the body, not in place: an inline script has no `defer`,
127
+ * so in the head it would run before #app exists
128
+ */
129
+ html.source = source.replace('</body>', () => `<script>${script}</script>\n</body>`);
130
+
131
+ }
132
+ };
133
+
134
+ }
@@ -0,0 +1,391 @@
1
+ /**
2
+ * SSR preflight for a design.
3
+ *
4
+ * Renders every page a design can produce through the real ssr build - the same
5
+ * dist/server/server-entry.js that gets published - and fails if anything in it
6
+ * throws, warns, or comes back empty.
7
+ *
8
+ * It exists because the production renderer is forgiving in the worst way:
9
+ * RenderClass.renderSingle collects the renders with Promise.allSettled and only
10
+ * console.errors a rejected one, so a page that blows up during SSR is silently
11
+ * published as an empty shell. This suite is the gate before that happens.
12
+ *
13
+ * Offline by design: everything comes from the build output and the fixtures
14
+ * bundle, nothing talks to an api.
15
+ */
16
+ import { readFile } from 'node:fs/promises';
17
+ import { resolve } from 'node:path';
18
+ import { describe, it } from 'node:test';
19
+ import assert from 'node:assert/strict';
20
+
21
+ const DEFAULT_PATHS = {
22
+ serverEntry : 'dist/server/server-entry.js',
23
+ manifest : 'dist/server/.vite/ssr-manifest.json',
24
+ template : 'dist/server/index.server.html',
25
+ mailTemplate: 'dist/server/index.mail.html',
26
+ };
27
+ /**
28
+ * the app logs its startup through mixins/logger, which is console.log - only
29
+ * warnings and errors are interesting here
30
+ */
31
+ const VUE_WARN = '[Vue warn]';
32
+
33
+ /**
34
+ * mirrors RenderClass.createGenericContext: the shape the STUDIO renderer hands
35
+ * to render(), minus the parts that only matter for asset urls.
36
+ */
37
+ function createContext (content, options = {}) {
38
+
39
+ return {
40
+ data : content,
41
+ layout : options.layout,
42
+ settings : {
43
+ name : content.company ? `${content.company}-${content.project}` : 'preflight',
44
+ company: content.company ?? 'preflight',
45
+ project: content.project ?? 'preflight',
46
+ mode : content.settings?.mode ?? 'presentation',
47
+ },
48
+ tags : content.tags,
49
+ cdn : '',
50
+ renderTarget: options.renderTarget ?? 'web',
51
+ };
52
+
53
+ }
54
+
55
+ /**
56
+ * turns a fixture into a site the renderer can address: a filename to look up
57
+ * and a url that resolves to it, exactly as Publish.action stores one.
58
+ */
59
+ function createSite (name, content, options = {}) {
60
+ const target = options.target ?? content.target;
61
+ const filename = target === 'mail'
62
+ ? ['mail']
63
+ : content.settings?.filename ?? [name];
64
+ const site = {
65
+ ...content,
66
+ id : content.id ?? name,
67
+ state : content.state ?? 'publish',
68
+ settings: {
69
+ mode : 'presentation',
70
+ title: name,
71
+ ...content.settings,
72
+ filename,
73
+ },
74
+ };
75
+
76
+ if (target) site.target = target;
77
+
78
+ return { name, site, url: site.settings.url || filename[0] };
79
+
80
+ }
81
+
82
+ /**
83
+ * vue reports a failed serverPrefetch and an unresolved component through the
84
+ * console rather than by rejecting, so the console is part of the assertion.
85
+ */
86
+ async function captureConsole (run) {
87
+
88
+ const captured = { errors: [], warnings: [] };
89
+ const originalError = console.error;
90
+ const originalWarn = console.warn;
91
+
92
+ console.error = (...args) => captured.errors.push(args.map(stringify).join(' '));
93
+ console.warn = (...args) => captured.warnings.push(args.map(stringify).join(' '));
94
+
95
+ try {
96
+
97
+ const result = await run();
98
+
99
+ return { result, captured };
100
+
101
+ } finally {
102
+
103
+ console.error = originalError;
104
+ console.warn = originalWarn;
105
+
106
+ }
107
+
108
+ }
109
+
110
+ function stringify (value) {
111
+
112
+ if (value instanceof Error) return `${value.message}\n${value.stack}`;
113
+ if (typeof value === 'string') return value;
114
+
115
+ try {
116
+
117
+ return JSON.stringify(value);
118
+
119
+ } catch {
120
+
121
+ return String(value);
122
+
123
+ }
124
+
125
+ }
126
+
127
+ function assembleDocument (template, rendered) {
128
+
129
+ return template
130
+ .replace('<!--preload-links-->', rendered.preloadLinks)
131
+ .replace('<!--ssr-outlet-->', rendered.html)
132
+ .replace('<!--pinia-state-->', `<script>window.__INITIAL_STATE__ = ${rendered.initialState}</script>`);
133
+
134
+ }
135
+
136
+ /**
137
+ * every marker a module could plausibly put on its root element. the class is a
138
+ * convention, not a contract - a handful of modules render no wrapper at all -
139
+ * so a miss is reported, never asserted.
140
+ */
141
+ function moduleMarkers (type, overrides = {}) {
142
+
143
+ if (overrides[type]) return overrides[type];
144
+
145
+ const base = type.replace(/-module$/, '');
146
+
147
+ return [`lila-${base}-module`, `${base}-module`, `lila-${base}`];
148
+
149
+ }
150
+
151
+ /**
152
+ * splits captured console output into what the suite must fail on and what the
153
+ * project has declared as expected - a page that needs data only the backend
154
+ * has (a structure declaration, a payment provider) complains offline, and that
155
+ * complaint is not a regression.
156
+ */
157
+ function splitAllowed (messages, allowed) {
158
+
159
+ const unexpected = [];
160
+ const expected = [];
161
+
162
+ messages.forEach((single) => {
163
+
164
+ if (allowed.some((pattern) => pattern.test(single))) {
165
+
166
+ expected.push(single);
167
+
168
+ } else {
169
+
170
+ unexpected.push(single);
171
+
172
+ }
173
+
174
+ });
175
+
176
+ return { unexpected, expected };
177
+
178
+ }
179
+
180
+ function assertRendered (name, rendered, captured, options = {}) {
181
+
182
+ const allowed = options.allowedConsole ?? [];
183
+ const errors = splitAllowed(captured.errors, allowed);
184
+ const warnings = splitAllowed(
185
+ captured.warnings.filter((single) => single.includes(VUE_WARN)),
186
+ allowed,
187
+ );
188
+
189
+ assert.ok(rendered, `${name}: render returned nothing`);
190
+ assert.equal(
191
+ errors.unexpected.length,
192
+ 0,
193
+ `${name}: console.error during render\n${errors.unexpected.join('\n')}`,
194
+ );
195
+ assert.equal(
196
+ warnings.unexpected.length,
197
+ 0,
198
+ `${name}: vue warnings during render\n${warnings.unexpected.join('\n')}`,
199
+ );
200
+
201
+ if (options.onExpected && (errors.expected.length || warnings.expected.length)) {
202
+
203
+ options.onExpected([...errors.expected, ...warnings.expected]);
204
+
205
+ }
206
+ assert.equal(typeof rendered.html, 'string', `${name}: html is not a string`);
207
+
208
+ const contentMarker = options.contentMarker ?? 'lila-content-module';
209
+
210
+ if (contentMarker) {
211
+
212
+ assert.ok(
213
+ rendered.html.includes(contentMarker),
214
+ `${name}: ${contentMarker} did not render - the page would publish as an empty shell`,
215
+ );
216
+
217
+ }
218
+
219
+ assert.doesNotThrow(
220
+ () => JSON.parse(rendered.initialState),
221
+ `${name}: initialState is not valid json, hydration would fail`,
222
+ );
223
+
224
+ }
225
+
226
+ async function loadRenderer (root, paths) {
227
+
228
+ const serverEntry = resolve(root, paths.serverEntry);
229
+ const renderer = await import(serverEntry);
230
+
231
+ assert.equal(typeof renderer.render, 'function', `${paths.serverEntry} does not export render()`);
232
+
233
+ return renderer;
234
+
235
+ }
236
+
237
+ async function loadJson (root, path) {
238
+
239
+ return JSON.parse(await readFile(resolve(root, path), 'utf-8'));
240
+
241
+ }
242
+
243
+ /**
244
+ * registers the whole preflight against node:test.
245
+ *
246
+ * @param {object} options
247
+ * @param {string} [options.root] project root, defaults to process.cwd()
248
+ * @param {object} options.fixtures the built fixtures bundle: { viewData, modules, mailModules }
249
+ * @param {object} [options.paths] overrides for the build output locations
250
+ * @param {boolean} [options.variants] also render every registered variant on its own, default true
251
+ * @param {RegExp[]} [options.allowedConsole] console output the project expects offline, reported instead of failed
252
+ * @param {Record<string, string[]>} [options.markers] root markers for modules whose class does not follow from their type
253
+ */
254
+ export async function runSsrSuite (options) {
255
+
256
+ const root = options.root ?? process.cwd();
257
+ const paths = { ...DEFAULT_PATHS, ...options.paths };
258
+ const fixtures = options.fixtures;
259
+ const withVariants = options.variants !== false;
260
+ const allowedConsole = options.allowedConsole ?? [];
261
+ const markers = options.markers ?? {};
262
+ const renderer = await loadRenderer(root, paths);
263
+ const manifest = await loadJson(root, paths.manifest);
264
+ const template = await readFile(resolve(root, paths.template), 'utf-8');
265
+
266
+ async function render (entry, renderOptions = {}) {
267
+
268
+ return captureConsole(() => renderer.render(
269
+ entry.url,
270
+ createContext(entry.site, renderOptions),
271
+ renderOptions.contextData ?? null,
272
+ renderOptions.recipient ?? null,
273
+ renderOptions.list ?? null,
274
+ manifest,
275
+ { cdnUrl: '' },
276
+ ));
277
+
278
+ }
279
+
280
+ describe('ssr: viewData renders as its own site', () => {
281
+
282
+ Object.entries(fixtures.viewData ?? {}).forEach(([name, content]) => {
283
+
284
+ it(name, async (t) => {
285
+
286
+ if (!Array.isArray(content?.modules)) {
287
+
288
+ t.diagnostic(`${name} is not page content (no modules array) - skipped`);
289
+ return;
290
+
291
+ }
292
+
293
+ const entry = createSite(name, content);
294
+ const { result, captured } = await render(entry);
295
+
296
+ assertRendered(name, result, captured, {
297
+ allowedConsole,
298
+ onExpected: (messages) => t.diagnostic(`${name}: expected offline complaints\n${messages.join('\n')}`),
299
+ });
300
+
301
+ const document = assembleDocument(template, result);
302
+
303
+ assert.ok(
304
+ !document.includes('<!--ssr-outlet-->'),
305
+ `${name}: the html template still holds its placeholder`,
306
+ );
307
+
308
+ content.modules.forEach((single) => {
309
+
310
+ if (!single?.type) return;
311
+
312
+ const found = moduleMarkers(single.type, markers).some((marker) => result.html.includes(marker));
313
+
314
+ if (!found) t.diagnostic(`${name}: no root marker found for ${single.type}`);
315
+
316
+ });
317
+
318
+ });
319
+
320
+ });
321
+
322
+ });
323
+
324
+ describe('ssr: every module renders with no data at all', () => {
325
+
326
+ (fixtures.modules ?? []).forEach((single) => {
327
+
328
+ const type = `${single.name}-module`;
329
+
330
+ it(type, async () => {
331
+
332
+ const entry = createSite(`empty-${single.name}`, {
333
+ modules: [{ type, uuid: `empty-${single.name}` }],
334
+ });
335
+ const { result, captured } = await render(entry);
336
+
337
+ assertRendered(type, result, captured, { allowedConsole });
338
+
339
+ });
340
+
341
+ if (!withVariants) return;
342
+
343
+ (single.variants ?? []).forEach((variant) => {
344
+
345
+ if (!variant?.key) return;
346
+
347
+ it(`${type} [${variant.key}]`, async () => {
348
+
349
+ const entry = createSite(`empty-${single.name}-${variant.key}`, {
350
+ modules: [{ type, uuid: `empty-${single.name}-${variant.key}`, variant: [variant.key] }],
351
+ });
352
+ const { result, captured } = await render(entry);
353
+
354
+ assertRendered(`${type} [${variant.key}]`, result, captured, { allowedConsole });
355
+
356
+ });
357
+
358
+ });
359
+
360
+ });
361
+
362
+ });
363
+
364
+ if (!fixtures.mailModules?.length) return;
365
+
366
+ describe('ssr: every mail module renders with no data at all', () => {
367
+
368
+ fixtures.mailModules.forEach((single) => {
369
+
370
+ const type = `${single.name}-module`;
371
+
372
+ it(type, async () => {
373
+
374
+ const entry = createSite(`empty-mail-${single.name}`, {
375
+ target : 'mail',
376
+ modules: [{ type, uuid: `empty-mail-${single.name}` }],
377
+ }, { target: 'mail' });
378
+ const { result, captured } = await render(entry);
379
+
380
+ assertRendered(type, result, captured, { allowedConsole });
381
+
382
+ });
383
+
384
+ });
385
+
386
+ });
387
+
388
+ }
389
+
390
+ export default runSsrSuite;
391
+ export { createSite, createContext, assembleDocument, captureConsole };
@@ -0,0 +1,24 @@
1
+ /** @type {import('stylelint').Config} */
2
+ export default {
3
+ 'extends': [
4
+ 'stylelint-config-html',
5
+ 'stylelint-config-recommended-vue'
6
+ ],
7
+ 'plugins': [
8
+ 'stylelint-order'
9
+ ],
10
+ 'overrides': [
11
+ {
12
+ 'files' : ['*.vue', '**/*.vue'],
13
+ 'customSyntax': 'postcss-html'
14
+ },
15
+ {
16
+ 'files' : ['*.less', '**/*.less'],
17
+ 'customSyntax': 'postcss-less'
18
+ }
19
+ ],
20
+ 'rules': {
21
+ 'media-query-no-invalid' : null,
22
+ 'declaration-property-value-no-unknown': null
23
+ }
24
+ }