meno-core 1.0.48 → 1.0.50
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/build-astro.ts +6 -2
- package/dist/build-static.js +7 -7
- package/dist/chunks/{chunk-D5E3OKSL.js → chunk-56EUSC6D.js} +5 -5
- package/dist/chunks/{chunk-3FHJUHAS.js → chunk-7NIC4I3V.js} +300 -43
- package/dist/chunks/chunk-7NIC4I3V.js.map +7 -0
- package/dist/chunks/{chunk-B2RTLDXY.js → chunk-AZQYF6KE.js} +132 -1
- package/dist/chunks/chunk-AZQYF6KE.js.map +7 -0
- package/dist/chunks/{chunk-TPQ7APVQ.js → chunk-CVLFID6V.js} +473 -73
- package/dist/chunks/chunk-CVLFID6V.js.map +7 -0
- package/dist/chunks/{chunk-NP76N4HQ.js → chunk-EDQSMAMP.js} +13 -2
- package/dist/chunks/{chunk-NP76N4HQ.js.map → chunk-EDQSMAMP.js.map} +2 -2
- package/dist/chunks/{chunk-RQSTH2BS.js → chunk-H4JSCDNW.js} +2 -2
- package/dist/chunks/{chunk-EK4KESLU.js → chunk-J23ZX5AP.js} +8 -2
- package/dist/chunks/{chunk-EK4KESLU.js.map → chunk-J23ZX5AP.js.map} +2 -2
- package/dist/chunks/{chunk-UUA5LEWF.js → chunk-LPVETICS.js} +156 -8
- package/dist/chunks/chunk-LPVETICS.js.map +7 -0
- package/dist/chunks/{chunk-BJRKEPMP.js → chunk-PQ2HRXDR.js} +5 -2
- package/dist/chunks/chunk-PQ2HRXDR.js.map +7 -0
- package/dist/chunks/{chunk-NKUV77SR.js → chunk-YWJJD5D6.js} +133 -37
- package/dist/chunks/chunk-YWJJD5D6.js.map +7 -0
- package/dist/chunks/{configService-IGJEC3MC.js → configService-VOY2MY2K.js} +3 -3
- package/dist/entries/server-router.js +9 -9
- package/dist/entries/server-router.js.map +2 -2
- package/dist/lib/client/index.js +92 -32
- package/dist/lib/client/index.js.map +3 -3
- package/dist/lib/server/index.js +14 -12
- package/dist/lib/server/index.js.map +2 -2
- package/dist/lib/shared/index.js +46 -10
- package/dist/lib/shared/index.js.map +3 -3
- package/entries/server-router.tsx +6 -2
- package/lib/client/core/ComponentBuilder.test.ts +34 -0
- package/lib/client/core/ComponentBuilder.ts +33 -4
- package/lib/client/core/builders/embedBuilder.ts +28 -7
- package/lib/client/core/builders/linkNodeBuilder.ts +28 -7
- package/lib/client/core/builders/localeListBuilder.ts +30 -11
- package/lib/client/styles/StyleInjector.ts +3 -2
- package/lib/client/templateEngine.ts +24 -0
- package/lib/client/theme.ts +4 -4
- package/lib/server/cssGenerator.test.ts +64 -1
- package/lib/server/cssGenerator.ts +48 -9
- package/lib/server/fileWatcher.test.ts +134 -0
- package/lib/server/fileWatcher.ts +100 -32
- package/lib/server/jsonLoader.ts +1 -0
- package/lib/server/providers/fileSystemCMSProvider.test.ts +163 -0
- package/lib/server/providers/fileSystemCMSProvider.ts +240 -19
- package/lib/server/routes/index.ts +1 -1
- package/lib/server/routes/pages.ts +23 -1
- package/lib/server/services/cmsService.test.ts +246 -0
- package/lib/server/services/cmsService.ts +122 -5
- package/lib/server/services/configService.ts +6 -0
- package/lib/server/services/fileWatcherService.ts +17 -0
- package/lib/server/ssr/attributeBuilder.ts +41 -0
- package/lib/server/ssr/htmlGenerator.test.ts +113 -0
- package/lib/server/ssr/htmlGenerator.ts +62 -7
- package/lib/server/ssr/liveReloadIntegration.test.ts +209 -0
- package/lib/server/ssr/ssrRenderer.test.ts +564 -0
- package/lib/server/ssr/ssrRenderer.ts +228 -49
- package/lib/server/webflow/buildWebflow.ts +1 -1
- package/lib/server/websocketManager.test.ts +61 -6
- package/lib/server/websocketManager.ts +25 -1
- package/lib/shared/cssGeneration.test.ts +267 -1
- package/lib/shared/cssGeneration.ts +240 -18
- package/lib/shared/cssProperties.test.ts +275 -1
- package/lib/shared/cssProperties.ts +223 -7
- package/lib/shared/interfaces/contentProvider.ts +39 -6
- package/lib/shared/pathSecurity.ts +16 -0
- package/lib/shared/responsiveScaling.test.ts +143 -0
- package/lib/shared/responsiveScaling.ts +253 -2
- package/lib/shared/themeDefaults.test.ts +3 -3
- package/lib/shared/themeDefaults.ts +3 -3
- package/lib/shared/types/api.ts +10 -1
- package/lib/shared/types/cms.ts +46 -12
- package/lib/shared/types/index.ts +1 -0
- package/lib/shared/utilityClassConfig.ts +3 -0
- package/lib/shared/utilityClassMapper.test.ts +123 -0
- package/lib/shared/utilityClassMapper.ts +179 -8
- package/lib/shared/validation/schemas.test.ts +93 -0
- package/lib/shared/validation/schemas.ts +71 -16
- package/lib/shared/validation/validators.ts +26 -1
- package/package.json +1 -1
- package/dist/chunks/chunk-3FHJUHAS.js.map +0 -7
- package/dist/chunks/chunk-B2RTLDXY.js.map +0 -7
- package/dist/chunks/chunk-BJRKEPMP.js.map +0 -7
- package/dist/chunks/chunk-NKUV77SR.js.map +0 -7
- package/dist/chunks/chunk-TPQ7APVQ.js.map +0 -7
- package/dist/chunks/chunk-UUA5LEWF.js.map +0 -7
- /package/dist/chunks/{chunk-D5E3OKSL.js.map → chunk-56EUSC6D.js.map} +0 -0
- /package/dist/chunks/{chunk-RQSTH2BS.js.map → chunk-H4JSCDNW.js.map} +0 -0
- /package/dist/chunks/{configService-IGJEC3MC.js.map → configService-VOY2MY2K.js.map} +0 -0
|
@@ -43,6 +43,7 @@ mock.module('./imageMetadata', () => ({
|
|
|
43
43
|
mock.module('../services/configService', () => ({
|
|
44
44
|
configService: {
|
|
45
45
|
getImageFormat: () => mockImageFormat,
|
|
46
|
+
getResponsiveScales: () => ({ enabled: false, baseReference: 16 }),
|
|
46
47
|
load: async () => {},
|
|
47
48
|
isLoaded: () => true,
|
|
48
49
|
reset: () => {},
|
|
@@ -440,6 +441,82 @@ describe('ssrRenderer', () => {
|
|
|
440
441
|
});
|
|
441
442
|
});
|
|
442
443
|
|
|
444
|
+
// -----------------------------------------------------------------------
|
|
445
|
+
// 6b. _i18n value resolution on node children + attributes
|
|
446
|
+
// -----------------------------------------------------------------------
|
|
447
|
+
describe('buildComponentHTML - _i18n on children and attributes', () => {
|
|
448
|
+
const en = { _i18n: true, en: 'Hello', pl: 'Cześć' };
|
|
449
|
+
const i18nConfig = {
|
|
450
|
+
defaultLocale: 'en',
|
|
451
|
+
locales: [
|
|
452
|
+
{ code: 'en', name: 'EN', nativeName: 'English', langTag: 'en-US' },
|
|
453
|
+
{ code: 'pl', name: 'PL', nativeName: 'Polski', langTag: 'pl-PL' },
|
|
454
|
+
],
|
|
455
|
+
};
|
|
456
|
+
|
|
457
|
+
test('resolves _i18n object as direct children for the default locale', async () => {
|
|
458
|
+
const node = { type: 'node', tag: 'h1', children: en };
|
|
459
|
+
const html = await render(node, { i18nConfig });
|
|
460
|
+
expect(html).toContain('Hello');
|
|
461
|
+
expect(html).not.toContain('Cześć');
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
test('resolves _i18n object as direct children for an explicit locale', async () => {
|
|
465
|
+
const node = { type: 'node', tag: 'h1', children: en };
|
|
466
|
+
const html = await render(node, { locale: 'pl', i18nConfig });
|
|
467
|
+
expect(html).toContain('Cześć');
|
|
468
|
+
expect(html).not.toContain('Hello');
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
test('resolves _i18n objects inside an array of children, preserving siblings', async () => {
|
|
472
|
+
const node = {
|
|
473
|
+
type: 'node',
|
|
474
|
+
tag: 'p',
|
|
475
|
+
children: [
|
|
476
|
+
en,
|
|
477
|
+
' literal ',
|
|
478
|
+
{ type: 'node', tag: 'span', children: 'static' },
|
|
479
|
+
],
|
|
480
|
+
};
|
|
481
|
+
const html = await render(node, { i18nConfig });
|
|
482
|
+
expect(html).toContain('Hello');
|
|
483
|
+
expect(html).toContain(' literal ');
|
|
484
|
+
expect(html).toContain('<span');
|
|
485
|
+
expect(html).toContain('static');
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
test('resolves _i18n object on attribute values', async () => {
|
|
489
|
+
const node = {
|
|
490
|
+
type: 'node',
|
|
491
|
+
tag: 'img',
|
|
492
|
+
attributes: {
|
|
493
|
+
src: '/x.png',
|
|
494
|
+
alt: { _i18n: true, en: 'Photo', pl: 'Zdjęcie' },
|
|
495
|
+
},
|
|
496
|
+
};
|
|
497
|
+
const html = await render(node, { locale: 'pl', i18nConfig });
|
|
498
|
+
expect(html).toContain('alt="Zdjęcie"');
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
test('falls back to defaultLocale when the active locale key is missing', async () => {
|
|
502
|
+
const valueWithoutDe = { _i18n: true, en: 'Hello', pl: 'Cześć' };
|
|
503
|
+
const node = { type: 'node', tag: 'h1', children: valueWithoutDe };
|
|
504
|
+
const html = await render(node, { locale: 'de', i18nConfig });
|
|
505
|
+
// resolveTranslation falls back to defaultLocale (en)
|
|
506
|
+
expect(html).toContain('Hello');
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
test('precedence: _i18n: true wins over a stray type/tag on the same object', async () => {
|
|
510
|
+
// If someone accidentally writes both _i18n and type on the same object,
|
|
511
|
+
// i18n wins — the renderer treats it as a localized string, not a node.
|
|
512
|
+
const ambiguous = { _i18n: true, type: 'node', tag: 'div', en: 'X', pl: 'Y' };
|
|
513
|
+
const node = { type: 'node', tag: 'h1', children: ambiguous };
|
|
514
|
+
const html = await render(node, { locale: 'pl', i18nConfig });
|
|
515
|
+
expect(html).toContain('Y');
|
|
516
|
+
expect(html).not.toContain('<div');
|
|
517
|
+
});
|
|
518
|
+
});
|
|
519
|
+
|
|
443
520
|
// -----------------------------------------------------------------------
|
|
444
521
|
// 7. Link nodes
|
|
445
522
|
// -----------------------------------------------------------------------
|
|
@@ -3109,6 +3186,203 @@ describe('ssrRenderer', () => {
|
|
|
3109
3186
|
expect(html).toContain('Author a1');
|
|
3110
3187
|
expect(html).toContain('Author a2');
|
|
3111
3188
|
});
|
|
3189
|
+
|
|
3190
|
+
test('inner list items dotted-ref resolves against host component prop', async () => {
|
|
3191
|
+
// {{config.0.tagIds}} is a dotted template — processStructure leaves it literal
|
|
3192
|
+
// (hasItemTemplates returns true for `\w+\.\w+`). The SSR items resolver must
|
|
3193
|
+
// see the host component's `config` prop in its scope.
|
|
3194
|
+
const calledWith: string[][] = [];
|
|
3195
|
+
const mockCmsService = {
|
|
3196
|
+
getItemsByIds: async (_collection: string, ids: string[]) => {
|
|
3197
|
+
calledWith.push(ids);
|
|
3198
|
+
return ids.map(id => ({ _id: id, name: `Tag ${id}` }));
|
|
3199
|
+
},
|
|
3200
|
+
getSchema: () => undefined,
|
|
3201
|
+
};
|
|
3202
|
+
|
|
3203
|
+
const Card: ComponentDefinition = {
|
|
3204
|
+
component: {
|
|
3205
|
+
interface: {
|
|
3206
|
+
config: { type: 'list' as any, default: [] },
|
|
3207
|
+
},
|
|
3208
|
+
structure: {
|
|
3209
|
+
type: 'list',
|
|
3210
|
+
sourceType: 'collection',
|
|
3211
|
+
source: 'tags',
|
|
3212
|
+
itemAs: 'tag',
|
|
3213
|
+
items: '{{config.0.tagIds}}',
|
|
3214
|
+
children: [
|
|
3215
|
+
{ type: 'node', tag: 'span', children: '{{tag.name}}' },
|
|
3216
|
+
],
|
|
3217
|
+
},
|
|
3218
|
+
},
|
|
3219
|
+
};
|
|
3220
|
+
|
|
3221
|
+
const node = {
|
|
3222
|
+
type: 'component',
|
|
3223
|
+
component: 'Card',
|
|
3224
|
+
props: {
|
|
3225
|
+
config: [{ tagIds: ['t1', 't2'] }],
|
|
3226
|
+
},
|
|
3227
|
+
};
|
|
3228
|
+
|
|
3229
|
+
const html = await render(node, {
|
|
3230
|
+
globalComponents: { Card },
|
|
3231
|
+
cmsService: mockCmsService,
|
|
3232
|
+
});
|
|
3233
|
+
|
|
3234
|
+
expect(calledWith).toEqual([['t1', 't2']]);
|
|
3235
|
+
expect(html).toContain('Tag t1');
|
|
3236
|
+
expect(html).toContain('Tag t2');
|
|
3237
|
+
});
|
|
3238
|
+
|
|
3239
|
+
test('inner list filter dotted-ref resolves against host component prop', async () => {
|
|
3240
|
+
// {{config.0.collection}} is a dotted template — processStructure leaves it literal.
|
|
3241
|
+
// SSR's resolveFilterTemplates must see the host component's `config` prop.
|
|
3242
|
+
const queries: any[] = [];
|
|
3243
|
+
const mockCmsService = {
|
|
3244
|
+
queryItems: async (q: any) => {
|
|
3245
|
+
queries.push(q);
|
|
3246
|
+
return [{ _id: 'p1', title: 'Hello' }];
|
|
3247
|
+
},
|
|
3248
|
+
getSchema: () => undefined,
|
|
3249
|
+
};
|
|
3250
|
+
|
|
3251
|
+
const Card: ComponentDefinition = {
|
|
3252
|
+
component: {
|
|
3253
|
+
interface: {
|
|
3254
|
+
config: { type: 'list' as any, default: [] },
|
|
3255
|
+
},
|
|
3256
|
+
structure: {
|
|
3257
|
+
type: 'list',
|
|
3258
|
+
sourceType: 'collection',
|
|
3259
|
+
source: 'posts',
|
|
3260
|
+
itemAs: 'post',
|
|
3261
|
+
filter: { category: '{{config.0.collection}}' },
|
|
3262
|
+
children: [
|
|
3263
|
+
{ type: 'node', tag: 'div', children: '{{post.title}}' },
|
|
3264
|
+
],
|
|
3265
|
+
},
|
|
3266
|
+
},
|
|
3267
|
+
};
|
|
3268
|
+
|
|
3269
|
+
const node = {
|
|
3270
|
+
type: 'component',
|
|
3271
|
+
component: 'Card',
|
|
3272
|
+
props: {
|
|
3273
|
+
config: [{ collection: 'news' }],
|
|
3274
|
+
},
|
|
3275
|
+
};
|
|
3276
|
+
|
|
3277
|
+
const html = await render(node, {
|
|
3278
|
+
globalComponents: { Card },
|
|
3279
|
+
cmsService: mockCmsService,
|
|
3280
|
+
});
|
|
3281
|
+
|
|
3282
|
+
expect(queries.length).toBe(1);
|
|
3283
|
+
expect(queries[0].filter).toEqual({ category: 'news' });
|
|
3284
|
+
expect(html).toContain('Hello');
|
|
3285
|
+
});
|
|
3286
|
+
|
|
3287
|
+
test('parent list loop variable wins over a colliding host component prop', async () => {
|
|
3288
|
+
// Outer CMS list iterates with itemAs: 'tag', producing a templateContext entry
|
|
3289
|
+
// named `tag` set to the current item. The host Card also receives a prop named
|
|
3290
|
+
// `tag` set to a fake value. The inner list's items: {{tag.id}} must resolve to
|
|
3291
|
+
// the parent loop variable's id (precedence: parent loop variable > component props).
|
|
3292
|
+
const calledWith: string[][] = [];
|
|
3293
|
+
const mockCmsService = {
|
|
3294
|
+
// Outer iteration: itemAs: 'tag' makes templateContext.tag = item, so the
|
|
3295
|
+
// inner template `{{tag.id}}` looks up item.id directly.
|
|
3296
|
+
queryItems: async () => [
|
|
3297
|
+
{ _id: 'p1', id: 'real-1' },
|
|
3298
|
+
{ _id: 'p2', id: 'real-2' },
|
|
3299
|
+
],
|
|
3300
|
+
getItemsByIds: async (_collection: string, ids: string[]) => {
|
|
3301
|
+
calledWith.push(ids);
|
|
3302
|
+
return ids.map(id => ({ _id: id, name: `Item ${id}` }));
|
|
3303
|
+
},
|
|
3304
|
+
getSchema: () => undefined,
|
|
3305
|
+
};
|
|
3306
|
+
|
|
3307
|
+
const Card: ComponentDefinition = {
|
|
3308
|
+
component: {
|
|
3309
|
+
interface: {
|
|
3310
|
+
// type: 'list' so the array prop value passes validation. The prop
|
|
3311
|
+
// `tag` will land in componentResolvedProps; the parent loop var
|
|
3312
|
+
// `tag` (set to the current outer item) will land in templateContext.
|
|
3313
|
+
tag: { type: 'list' as any, default: [] },
|
|
3314
|
+
},
|
|
3315
|
+
structure: {
|
|
3316
|
+
type: 'list',
|
|
3317
|
+
sourceType: 'collection',
|
|
3318
|
+
source: 'tags',
|
|
3319
|
+
itemAs: 'inner',
|
|
3320
|
+
items: '{{tag.id}}',
|
|
3321
|
+
children: [
|
|
3322
|
+
{ type: 'node', tag: 'span', children: '{{inner.name}}' },
|
|
3323
|
+
],
|
|
3324
|
+
},
|
|
3325
|
+
},
|
|
3326
|
+
};
|
|
3327
|
+
|
|
3328
|
+
const node = {
|
|
3329
|
+
type: 'list',
|
|
3330
|
+
sourceType: 'collection',
|
|
3331
|
+
source: 'posts',
|
|
3332
|
+
itemAs: 'tag',
|
|
3333
|
+
children: [
|
|
3334
|
+
{
|
|
3335
|
+
type: 'component',
|
|
3336
|
+
component: 'Card',
|
|
3337
|
+
props: {
|
|
3338
|
+
// Static fake value for the colliding prop; the inner template
|
|
3339
|
+
// must resolve to the parent loop var, not this.
|
|
3340
|
+
tag: [{ id: 'fake-prop-id' }],
|
|
3341
|
+
},
|
|
3342
|
+
},
|
|
3343
|
+
],
|
|
3344
|
+
};
|
|
3345
|
+
|
|
3346
|
+
await render(node, {
|
|
3347
|
+
globalComponents: { Card },
|
|
3348
|
+
cmsService: mockCmsService,
|
|
3349
|
+
});
|
|
3350
|
+
|
|
3351
|
+
expect(calledWith).toEqual([['real-1'], ['real-2']]);
|
|
3352
|
+
});
|
|
3353
|
+
|
|
3354
|
+
test('page-root filter does not silently resolve {{cms.X}} (regression)', async () => {
|
|
3355
|
+
// Without a host component, ctx.componentResolvedProps is undefined and
|
|
3356
|
+
// ctx.templateContext is undefined → buildListResolutionScope returns
|
|
3357
|
+
// undefined → resolveFilterTemplates leaves filter values literal.
|
|
3358
|
+
// This pins down the documented behavior: the cmsContext namespace does
|
|
3359
|
+
// NOT silently merge into filter scope.
|
|
3360
|
+
const queries: any[] = [];
|
|
3361
|
+
const mockCmsService = {
|
|
3362
|
+
queryItems: async (q: any) => {
|
|
3363
|
+
queries.push(q);
|
|
3364
|
+
return [];
|
|
3365
|
+
},
|
|
3366
|
+
getSchema: () => undefined,
|
|
3367
|
+
};
|
|
3368
|
+
|
|
3369
|
+
const node = {
|
|
3370
|
+
type: 'list',
|
|
3371
|
+
sourceType: 'collection',
|
|
3372
|
+
source: 'posts',
|
|
3373
|
+
filter: { category: '{{cms.something}}' },
|
|
3374
|
+
children: [{ type: 'node', tag: 'div', children: '{{post.title}}' }],
|
|
3375
|
+
};
|
|
3376
|
+
|
|
3377
|
+
// cmsContext is provided, but page-root filter must not pull from it.
|
|
3378
|
+
await render(node, {
|
|
3379
|
+
cmsService: mockCmsService,
|
|
3380
|
+
cmsContext: { cms: { something: 'shouldNotBeUsed' } },
|
|
3381
|
+
});
|
|
3382
|
+
|
|
3383
|
+
expect(queries.length).toBe(1);
|
|
3384
|
+
expect(queries[0].filter).toEqual({ category: '{{cms.something}}' });
|
|
3385
|
+
});
|
|
3112
3386
|
});
|
|
3113
3387
|
|
|
3114
3388
|
// -----------------------------------------------------------------------
|
|
@@ -3862,6 +4136,188 @@ describe('ssrRenderer', () => {
|
|
|
3862
4136
|
});
|
|
3863
4137
|
});
|
|
3864
4138
|
|
|
4139
|
+
// ---------------------------------------------------------------------------
|
|
4140
|
+
// List with sourceType: 'prop' placed inside another component's slot
|
|
4141
|
+
// ---------------------------------------------------------------------------
|
|
4142
|
+
describe('buildComponentHTML - prop-source list inside slotted component', () => {
|
|
4143
|
+
test('list with bare prop-name source resolves against host props when inside a slot', async () => {
|
|
4144
|
+
// Section: a simple slot host (mirrors the user's Section component)
|
|
4145
|
+
const Section: ComponentDefinition = {
|
|
4146
|
+
component: {
|
|
4147
|
+
interface: {
|
|
4148
|
+
theme: { type: 'string', default: 'blue' },
|
|
4149
|
+
},
|
|
4150
|
+
structure: {
|
|
4151
|
+
type: 'node',
|
|
4152
|
+
tag: 'section',
|
|
4153
|
+
children: [
|
|
4154
|
+
{ type: 'slot' },
|
|
4155
|
+
],
|
|
4156
|
+
},
|
|
4157
|
+
},
|
|
4158
|
+
};
|
|
4159
|
+
|
|
4160
|
+
// Host: defines `testimonials`, renders a Section whose slot contains
|
|
4161
|
+
// a list bound to the host's `testimonials` prop with a bare name.
|
|
4162
|
+
const Host: ComponentDefinition = {
|
|
4163
|
+
component: {
|
|
4164
|
+
interface: {
|
|
4165
|
+
testimonials: { type: 'list' as any, default: [] },
|
|
4166
|
+
},
|
|
4167
|
+
structure: {
|
|
4168
|
+
type: 'node',
|
|
4169
|
+
tag: 'div',
|
|
4170
|
+
children: [
|
|
4171
|
+
{
|
|
4172
|
+
type: 'component',
|
|
4173
|
+
component: 'Section',
|
|
4174
|
+
props: { theme: 'blue' },
|
|
4175
|
+
children: [
|
|
4176
|
+
{
|
|
4177
|
+
type: 'list',
|
|
4178
|
+
sourceType: 'prop',
|
|
4179
|
+
source: 'testimonials',
|
|
4180
|
+
children: [
|
|
4181
|
+
{ type: 'node', tag: 'p', children: '{{item.name}}' },
|
|
4182
|
+
],
|
|
4183
|
+
},
|
|
4184
|
+
],
|
|
4185
|
+
},
|
|
4186
|
+
],
|
|
4187
|
+
},
|
|
4188
|
+
},
|
|
4189
|
+
};
|
|
4190
|
+
|
|
4191
|
+
const node = {
|
|
4192
|
+
type: 'component',
|
|
4193
|
+
component: 'Host',
|
|
4194
|
+
props: {
|
|
4195
|
+
testimonials: [
|
|
4196
|
+
{ name: 'Alice' },
|
|
4197
|
+
{ name: 'Bob' },
|
|
4198
|
+
{ name: 'Carol' },
|
|
4199
|
+
],
|
|
4200
|
+
},
|
|
4201
|
+
};
|
|
4202
|
+
|
|
4203
|
+
const html = await render(node, { globalComponents: { Host, Section } });
|
|
4204
|
+
expect(html).toContain('<section');
|
|
4205
|
+
expect(html).toContain('Alice');
|
|
4206
|
+
expect(html).toContain('Bob');
|
|
4207
|
+
expect(html).toContain('Carol');
|
|
4208
|
+
});
|
|
4209
|
+
|
|
4210
|
+
test('list with {{template}} source also resolves inside a slot', async () => {
|
|
4211
|
+
const Section: ComponentDefinition = {
|
|
4212
|
+
component: {
|
|
4213
|
+
interface: {},
|
|
4214
|
+
structure: {
|
|
4215
|
+
type: 'node',
|
|
4216
|
+
tag: 'section',
|
|
4217
|
+
children: [{ type: 'slot' }],
|
|
4218
|
+
},
|
|
4219
|
+
},
|
|
4220
|
+
};
|
|
4221
|
+
|
|
4222
|
+
const Host: ComponentDefinition = {
|
|
4223
|
+
component: {
|
|
4224
|
+
interface: {
|
|
4225
|
+
testimonials: { type: 'list' as any, default: [] },
|
|
4226
|
+
},
|
|
4227
|
+
structure: {
|
|
4228
|
+
type: 'component',
|
|
4229
|
+
component: 'Section',
|
|
4230
|
+
children: [
|
|
4231
|
+
{
|
|
4232
|
+
type: 'list',
|
|
4233
|
+
sourceType: 'prop',
|
|
4234
|
+
source: '{{testimonials}}',
|
|
4235
|
+
children: [
|
|
4236
|
+
{ type: 'node', tag: 'p', children: '{{item.name}}' },
|
|
4237
|
+
],
|
|
4238
|
+
},
|
|
4239
|
+
],
|
|
4240
|
+
},
|
|
4241
|
+
},
|
|
4242
|
+
};
|
|
4243
|
+
|
|
4244
|
+
const node = {
|
|
4245
|
+
type: 'component',
|
|
4246
|
+
component: 'Host',
|
|
4247
|
+
props: { testimonials: [{ name: 'Dana' }] },
|
|
4248
|
+
};
|
|
4249
|
+
|
|
4250
|
+
const html = await render(node, { globalComponents: { Host, Section } });
|
|
4251
|
+
expect(html).toContain('Dana');
|
|
4252
|
+
});
|
|
4253
|
+
|
|
4254
|
+
test('bare prop-name source still works when list is in component own structure (no slot)', async () => {
|
|
4255
|
+
const Direct: ComponentDefinition = {
|
|
4256
|
+
component: {
|
|
4257
|
+
interface: {
|
|
4258
|
+
items: { type: 'list' as any, default: [] },
|
|
4259
|
+
},
|
|
4260
|
+
structure: {
|
|
4261
|
+
type: 'list',
|
|
4262
|
+
sourceType: 'prop',
|
|
4263
|
+
source: 'items',
|
|
4264
|
+
children: [
|
|
4265
|
+
{ type: 'node', tag: 'li', children: '{{item.label}}' },
|
|
4266
|
+
],
|
|
4267
|
+
},
|
|
4268
|
+
},
|
|
4269
|
+
};
|
|
4270
|
+
|
|
4271
|
+
const node = {
|
|
4272
|
+
type: 'component',
|
|
4273
|
+
component: 'Direct',
|
|
4274
|
+
props: { items: [{ label: 'Eve' }, { label: 'Frank' }] },
|
|
4275
|
+
};
|
|
4276
|
+
|
|
4277
|
+
const html = await render(node, { globalComponents: { Direct } });
|
|
4278
|
+
expect(html).toContain('Eve');
|
|
4279
|
+
expect(html).toContain('Frank');
|
|
4280
|
+
});
|
|
4281
|
+
|
|
4282
|
+
test('list source stays a string and renders empty when host has no matching prop', async () => {
|
|
4283
|
+
const Section: ComponentDefinition = {
|
|
4284
|
+
component: {
|
|
4285
|
+
interface: {},
|
|
4286
|
+
structure: {
|
|
4287
|
+
type: 'node',
|
|
4288
|
+
tag: 'section',
|
|
4289
|
+
children: [{ type: 'slot' }],
|
|
4290
|
+
},
|
|
4291
|
+
},
|
|
4292
|
+
};
|
|
4293
|
+
|
|
4294
|
+
const Host: ComponentDefinition = {
|
|
4295
|
+
component: {
|
|
4296
|
+
interface: {},
|
|
4297
|
+
structure: {
|
|
4298
|
+
type: 'component',
|
|
4299
|
+
component: 'Section',
|
|
4300
|
+
children: [
|
|
4301
|
+
{
|
|
4302
|
+
type: 'list',
|
|
4303
|
+
sourceType: 'prop',
|
|
4304
|
+
source: 'missing',
|
|
4305
|
+
children: [
|
|
4306
|
+
{ type: 'node', tag: 'p', children: '{{item}}' },
|
|
4307
|
+
],
|
|
4308
|
+
},
|
|
4309
|
+
],
|
|
4310
|
+
},
|
|
4311
|
+
},
|
|
4312
|
+
};
|
|
4313
|
+
|
|
4314
|
+
const node = { type: 'component', component: 'Host' };
|
|
4315
|
+
const html = await render(node, { globalComponents: { Host, Section } });
|
|
4316
|
+
// No items - list renders empty
|
|
4317
|
+
expect(html).not.toContain('<p');
|
|
4318
|
+
});
|
|
4319
|
+
});
|
|
4320
|
+
|
|
3865
4321
|
describe('CMS link localization', () => {
|
|
3866
4322
|
const i18nConfig = {
|
|
3867
4323
|
defaultLocale: 'en',
|
|
@@ -4019,4 +4475,112 @@ describe('ssrRenderer', () => {
|
|
|
4019
4475
|
expect(html).toContain('href="/fr/a-propos"');
|
|
4020
4476
|
});
|
|
4021
4477
|
});
|
|
4478
|
+
|
|
4479
|
+
// Editor attrs are emitted only when buildComponentHTML's injectEditorAttrs flag is on.
|
|
4480
|
+
// These attributes (data-element-path, data-cms-item-index, data-cms-context,
|
|
4481
|
+
// data-component-root, data-parent-component, data-component-context) drive
|
|
4482
|
+
// XRayOverlay and the click-to-select handler in static (SSR) preview mode.
|
|
4483
|
+
describe('buildComponentHTML - editor attrs (preview-only)', () => {
|
|
4484
|
+
test('omits data-element-path by default', async () => {
|
|
4485
|
+
const node = { type: 'node', tag: 'div', children: ['hello'] };
|
|
4486
|
+
const result = await buildComponentHTML(node as any);
|
|
4487
|
+
expect(result.html).not.toContain('data-element-path');
|
|
4488
|
+
});
|
|
4489
|
+
|
|
4490
|
+
test('emits data-element-path on every element when injectEditorAttrs is true', async () => {
|
|
4491
|
+
const node = {
|
|
4492
|
+
type: 'node',
|
|
4493
|
+
tag: 'div',
|
|
4494
|
+
children: [
|
|
4495
|
+
{ type: 'node', tag: 'span', children: ['hi'] },
|
|
4496
|
+
{ type: 'node', tag: 'a', attributes: { href: '/x' }, children: ['link'] },
|
|
4497
|
+
],
|
|
4498
|
+
};
|
|
4499
|
+
const result = await buildComponentHTML(
|
|
4500
|
+
node as any,
|
|
4501
|
+
{}, {}, undefined, undefined, undefined, undefined, undefined, undefined, undefined, true,
|
|
4502
|
+
);
|
|
4503
|
+
expect(result.html).toContain('data-element-path="0"');
|
|
4504
|
+
expect(result.html).toContain('data-element-path="0,0"');
|
|
4505
|
+
expect(result.html).toContain('data-element-path="0,1"');
|
|
4506
|
+
});
|
|
4507
|
+
|
|
4508
|
+
test('emits data-cms-item-index and data-cms-context for elements inside a list', async () => {
|
|
4509
|
+
// List in prop mode under a component resolves items from componentResolvedProps.
|
|
4510
|
+
const Card: ComponentDefinition = {
|
|
4511
|
+
component: {
|
|
4512
|
+
interface: {
|
|
4513
|
+
items: { type: 'list', default: [{ label: 'A' }, { label: 'B' }] } as any,
|
|
4514
|
+
},
|
|
4515
|
+
structure: {
|
|
4516
|
+
type: 'node',
|
|
4517
|
+
tag: 'div',
|
|
4518
|
+
children: [{
|
|
4519
|
+
type: 'list',
|
|
4520
|
+
sourceType: 'prop',
|
|
4521
|
+
source: 'items',
|
|
4522
|
+
children: [{ type: 'node', tag: 'p', children: '{{item.label}}' }],
|
|
4523
|
+
}],
|
|
4524
|
+
},
|
|
4525
|
+
},
|
|
4526
|
+
};
|
|
4527
|
+
const result = await buildComponentHTML(
|
|
4528
|
+
{ type: 'component', component: 'Card' } as any,
|
|
4529
|
+
{ Card },
|
|
4530
|
+
{}, undefined, undefined, undefined, undefined, undefined, undefined, undefined, true,
|
|
4531
|
+
);
|
|
4532
|
+
expect(result.html).toContain('data-cms-item-index="0"');
|
|
4533
|
+
expect(result.html).toContain('data-cms-item-index="1"');
|
|
4534
|
+
expect(result.html).toContain('data-cms-context=');
|
|
4535
|
+
});
|
|
4536
|
+
|
|
4537
|
+
test('marks the component instance root with data-component-root', async () => {
|
|
4538
|
+
const Card: ComponentDefinition = {
|
|
4539
|
+
component: {
|
|
4540
|
+
interface: {},
|
|
4541
|
+
structure: { type: 'node', tag: 'div', children: ['inner'] },
|
|
4542
|
+
},
|
|
4543
|
+
};
|
|
4544
|
+
const result = await buildComponentHTML(
|
|
4545
|
+
{ type: 'component', component: 'Card' } as any,
|
|
4546
|
+
{ Card },
|
|
4547
|
+
{}, undefined, undefined, undefined, undefined, undefined, undefined, undefined, true,
|
|
4548
|
+
);
|
|
4549
|
+
expect(result.html).toContain('data-component-root="true"');
|
|
4550
|
+
expect(result.html).toContain('data-component-context="Card"');
|
|
4551
|
+
});
|
|
4552
|
+
|
|
4553
|
+
test('emits page-absolute data-element-path for elements inside a component', async () => {
|
|
4554
|
+
// Component has a single wrapper <div> with a <span> child. The page hosts
|
|
4555
|
+
// the component as the only child of its root <div>, so:
|
|
4556
|
+
// page root <div> → [0]
|
|
4557
|
+
// Card root <div> → [0,0]
|
|
4558
|
+
// inner <span> → [0,0,0]
|
|
4559
|
+
// Before the fix SSR reset elementPath inside components, so the inner
|
|
4560
|
+
// span landed at [0] (component-relative) and XRay couldn't find it.
|
|
4561
|
+
const Card: ComponentDefinition = {
|
|
4562
|
+
component: {
|
|
4563
|
+
interface: {},
|
|
4564
|
+
structure: {
|
|
4565
|
+
type: 'node',
|
|
4566
|
+
tag: 'div',
|
|
4567
|
+
children: [{ type: 'node', tag: 'span', children: 'inner' }],
|
|
4568
|
+
},
|
|
4569
|
+
},
|
|
4570
|
+
};
|
|
4571
|
+
const page = {
|
|
4572
|
+
type: 'node',
|
|
4573
|
+
tag: 'div',
|
|
4574
|
+
children: [{ type: 'component', component: 'Card' }],
|
|
4575
|
+
};
|
|
4576
|
+
const result = await buildComponentHTML(
|
|
4577
|
+
page as any,
|
|
4578
|
+
{ Card },
|
|
4579
|
+
{}, undefined, undefined, undefined, undefined, undefined, undefined, undefined, true,
|
|
4580
|
+
);
|
|
4581
|
+
expect(result.html).toContain('data-element-path="0"');
|
|
4582
|
+
expect(result.html).toContain('data-element-path="0,0"');
|
|
4583
|
+
expect(result.html).toContain('data-element-path="0,0,0"');
|
|
4584
|
+
});
|
|
4585
|
+
});
|
|
4022
4586
|
});
|