@ethlete/contentful 4.0.0-next.7 → 4.0.0-next.9

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.
@@ -1,4 +1,4 @@
1
- import { formatFiles, updateJson } from '@nx/devkit';
1
+ import { formatFiles, installPackagesTask, readJson, updateJson } from '@nx/devkit';
2
2
  import { createMigrationScope } from './migration-scope.js';
3
3
  export const CONTENTFUL_V5_REPORT_PATH = 'contentful-v5-migration-tasks.md';
4
4
  const COMPONENTS_PACKAGE = '@ethlete/components';
@@ -24,15 +24,55 @@ const REMOVED_EXPORTS = [
24
24
  'isExecutedHtmlOrTextCommandCacheItem',
25
25
  'isExecutedTextCommandCacheItem',
26
26
  ];
27
- const IMAGE_TAG_REGEX = /<et-contentful-image\b[^>]*>/g;
28
27
  const INLINE_TEMPLATE_REGEX = /template\s*:\s*`([\s\S]*?)`/g;
29
- const CONTENTFUL_IMPORT_REGEX = /import\s+(?:type\s+)?\{([^}]*)\}\s+from\s+['"]@ethlete\/contentful['"]/g;
28
+ const CONTENTFUL_NAMED_REFERENCE_REGEX = /(?:import|export)\s+(?:type\s+)?(?:[\w$]+\s*,\s*)?\{([\s\S]*?)\}\s+from\s+['"]@ethlete\/contentful['"]/g;
29
+ const CONTENTFUL_NAMESPACE_REFERENCE_REGEX = /(?:import\s+\*\s+as\s+([\w$]+)|export\s+\*)\s+from\s+['"]@ethlete\/contentful['"]/g;
30
30
  const lineOf = (source, index) => source.slice(0, index).split('\n').length;
31
+ const replaceImageTags = (source, replace) => {
32
+ const tagStart = '<et-contentful-image';
33
+ let result = '';
34
+ let cursor = 0;
35
+ while (cursor < source.length) {
36
+ const start = source.indexOf(tagStart, cursor);
37
+ if (start === -1) {
38
+ result += source.slice(cursor);
39
+ break;
40
+ }
41
+ const boundary = source[start + tagStart.length];
42
+ if (boundary && !/[\s/>]/.test(boundary)) {
43
+ result += source.slice(cursor, start + tagStart.length);
44
+ cursor = start + tagStart.length;
45
+ continue;
46
+ }
47
+ let quote = null;
48
+ let end = start + tagStart.length;
49
+ for (; end < source.length; end++) {
50
+ const character = source[end];
51
+ if (quote) {
52
+ if (character === quote)
53
+ quote = null;
54
+ }
55
+ else if (character === '"' || character === "'" || character === '`') {
56
+ quote = character;
57
+ }
58
+ else if (character === '>') {
59
+ break;
60
+ }
61
+ }
62
+ if (end === source.length) {
63
+ result += source.slice(cursor);
64
+ break;
65
+ }
66
+ result += source.slice(cursor, start) + replace(source.slice(start, end + 1), start);
67
+ cursor = end + 1;
68
+ }
69
+ return result;
70
+ };
31
71
  /** Renames `hasPriority` to `priority` and collects removed class inputs, on `et-contentful-image` only. */
32
72
  const migrateTemplate = (template) => {
33
73
  let changed = false;
34
74
  const classInputHits = [];
35
- const content = template.replace(IMAGE_TAG_REGEX, (tag, offset) => {
75
+ const content = replaceImageTags(template, (tag, offset) => {
36
76
  const foundInputs = REMOVED_CLASS_INPUTS.filter((input) => new RegExp(`(^|[\\s[])${input}(\\]|=|(?=[\\s/>]))`).test(tag));
37
77
  if (foundInputs.length > 0) {
38
78
  classInputHits.push({ line: lineOf(template, offset), inputs: foundInputs });
@@ -58,8 +98,7 @@ const createClassInputTask = (filePath, hit) => ({
58
98
  .map((input) => `\`${input}\``)
59
99
  .join(', ')}. Those inputs were removed — the rendered picture now only carries static \`et-picture-*\` classes. Drop the binding and target the \`et-picture-*\` classes from your own CSS instead.`,
60
100
  });
61
- /** Matches a whole `useTailwindClasses: <literal>,` property line — the option was removed in v5. */
62
- const USE_TAILWIND_CLASSES_REGEX = /^[ \t]*useTailwindClasses\s*:\s*(?:true|false|[\w.]+)\s*,?[ \t]*\r?\n/gm;
101
+ const USE_TAILWIND_CLASSES_REGEX = /^[ \t]*useTailwindClasses\s*:\s*(?:true|false|[\w.$]+)\s*,?[ \t]*\r?\n|\buseTailwindClasses\s*:\s*(?:true|false|[\w.$]+)\s*,[ \t]*|,\s*useTailwindClasses\s*:\s*(?:true|false|[\w.$]+)(?=\s*[}\n])/gm;
63
102
  const migrateTsFile = (filePath, source) => {
64
103
  const tasks = [];
65
104
  let changed = false;
@@ -78,7 +117,7 @@ const migrateTsFile = (filePath, source) => {
78
117
  changed = true;
79
118
  return match.replace(template, () => result.content);
80
119
  });
81
- for (const importMatch of source.matchAll(CONTENTFUL_IMPORT_REGEX)) {
120
+ for (const importMatch of source.matchAll(CONTENTFUL_NAMED_REFERENCE_REGEX)) {
82
121
  const symbols = (importMatch[1] ?? '')
83
122
  .split(',')
84
123
  .map((specifier) => specifier
@@ -98,36 +137,54 @@ const migrateTsFile = (filePath, source) => {
98
137
  });
99
138
  }
100
139
  }
140
+ for (const namespaceMatch of source.matchAll(CONTENTFUL_NAMESPACE_REFERENCE_REGEX)) {
141
+ const namespace = namespaceMatch[1] ?? '*';
142
+ tasks.push({
143
+ id: `removed-export:${filePath}:${namespace}`,
144
+ file: filePath,
145
+ line: lineOf(source, namespaceMatch.index ?? 0),
146
+ message: `The ${namespace === '*' ? 'star re-export' : `\`${namespace}\` namespace import`} may reference renderer internals that are no longer public API. Replace it with explicit supported imports and remove any command-helper usage.`,
147
+ });
148
+ }
101
149
  return { content, changed, tasks };
102
150
  };
103
151
  const migratePackageJson = (tree, filePath) => {
104
152
  const tasks = [];
105
153
  let changed = false;
106
- updateJson(tree, filePath, (json) => {
107
- const dependencies = json['dependencies'];
108
- const peerDependencies = json['peerDependencies'];
109
- const section = dependencies?.['@ethlete/contentful']
110
- ? 'dependencies'
111
- : peerDependencies?.['@ethlete/contentful']
112
- ? 'peerDependencies'
113
- : null;
114
- if (!section)
115
- return json;
116
- const hasComponents = Boolean(dependencies?.[COMPONENTS_PACKAGE] ?? peerDependencies?.[COMPONENTS_PACKAGE]);
117
- if (!hasComponents) {
118
- json[section] = { ...json[section], [COMPONENTS_PACKAGE]: COMPONENTS_VERSION };
119
- changed = true;
120
- console.log(` ✓ ${filePath}: added ${COMPONENTS_PACKAGE}@${COMPONENTS_VERSION} to ${section}`);
121
- }
122
- if (dependencies?.['@ethlete/cdk'] ?? peerDependencies?.['@ethlete/cdk']) {
123
- tasks.push({
124
- id: `cdk-dependency:${filePath}`,
125
- file: filePath,
126
- message: '`@ethlete/contentful` no longer depends on `@ethlete/cdk`. The dependency was left in place because you may use it directly — if nothing in this project imports `@ethlete/cdk`, remove it.',
127
- });
128
- }
129
- return json;
130
- });
154
+ const source = tree.read(filePath, 'utf-8');
155
+ if (!source) {
156
+ return { changed, tasks };
157
+ }
158
+ if (!source.includes('@ethlete/contentful')) {
159
+ return { changed, tasks };
160
+ }
161
+ const json = readJson(tree, filePath);
162
+ const dependencies = json['dependencies'];
163
+ const peerDependencies = json['peerDependencies'];
164
+ const section = dependencies?.['@ethlete/contentful']
165
+ ? 'dependencies'
166
+ : peerDependencies?.['@ethlete/contentful']
167
+ ? 'peerDependencies'
168
+ : null;
169
+ if (!section) {
170
+ return { changed, tasks };
171
+ }
172
+ const hasComponents = Boolean(dependencies?.[COMPONENTS_PACKAGE] ?? peerDependencies?.[COMPONENTS_PACKAGE]);
173
+ if (!hasComponents) {
174
+ updateJson(tree, filePath, (packageJson) => {
175
+ packageJson[section] = { ...packageJson[section], [COMPONENTS_PACKAGE]: COMPONENTS_VERSION };
176
+ return packageJson;
177
+ });
178
+ changed = true;
179
+ console.log(` ✓ ${filePath}: added ${COMPONENTS_PACKAGE}@${COMPONENTS_VERSION} to ${section}`);
180
+ }
181
+ if (dependencies?.['@ethlete/cdk'] ?? peerDependencies?.['@ethlete/cdk']) {
182
+ tasks.push({
183
+ id: `cdk-dependency:${filePath}`,
184
+ file: filePath,
185
+ message: '`@ethlete/contentful` no longer depends on `@ethlete/cdk`. The dependency was left in place because you may use it directly — if nothing in this project imports `@ethlete/cdk`, remove it.',
186
+ });
187
+ }
131
188
  return { changed, tasks };
132
189
  };
133
190
  const renderReport = (tasks) => [
@@ -153,6 +210,7 @@ export default async function migrateToContentfulV5(tree, schema) {
153
210
  console.log(` Scope: ${scope.describe()}`);
154
211
  const tasks = [];
155
212
  let filesChanged = 0;
213
+ let dependenciesChanged = false;
156
214
  scope.visit(tree, (filePath) => {
157
215
  if (filePath === CONTENTFUL_V5_REPORT_PATH)
158
216
  return;
@@ -161,6 +219,8 @@ export default async function migrateToContentfulV5(tree, schema) {
161
219
  tasks.push(...result.tasks);
162
220
  if (result.changed)
163
221
  filesChanged++;
222
+ if (result.changed)
223
+ dependenciesChanged = true;
164
224
  return;
165
225
  }
166
226
  const isHtml = filePath.endsWith('.html');
@@ -188,5 +248,9 @@ export default async function migrateToContentfulV5(tree, schema) {
188
248
  if (tasks.length > 0) {
189
249
  console.log(`⚠️ ${tasks.length} site(s) need a manual decision — see ${CONTENTFUL_V5_REPORT_PATH}.`);
190
250
  }
251
+ if (dependenciesChanged) {
252
+ return (() => installPackagesTask(tree, true));
253
+ }
254
+ return undefined;
191
255
  }
192
256
  //# sourceMappingURL=migration.js.map
@@ -0,0 +1,12 @@
1
+ {
2
+ "migrations": [
3
+ {
4
+ "name": "to-contentful-v5",
5
+ "version": "4.0.0-next.7",
6
+ "kind": "auto",
7
+ "description": "Move to the components picture, the renamed image inputs and the removed renderer internals.",
8
+ "generator": "@ethlete/contentful:migrate-to-contentful-v5",
9
+ "docs": "/contentful/"
10
+ }
11
+ ]
12
+ }
package/package.json CHANGED
@@ -1,10 +1,9 @@
1
1
  {
2
2
  "name": "@ethlete/contentful",
3
- "version": "4.0.0-next.7",
3
+ "version": "4.0.0-next.9",
4
4
  "license": "MIT",
5
5
  "sideEffects": false,
6
6
  "peerDependencies": {
7
- "@analogjs/vite-plugin-angular": "2.6.3",
8
7
  "@angular/common": "22.0.7",
9
8
  "@angular/core": "22.0.7",
10
9
  "@angular/router": "22.0.7",
@@ -12,20 +11,16 @@
12
11
  "@ethlete/components": "^1.0.0-next.32",
13
12
  "@ethlete/core": "^5.0.0-beta.11",
14
13
  "@ethlete/query": "^6.0.0-beta.8",
15
- "@nx/devkit": "23.1.0",
16
- "vite": "8.1.5"
14
+ "@nx/devkit": "23.1.0"
17
15
  },
18
16
  "peerDependenciesMeta": {
19
- "@analogjs/vite-plugin-angular": {
20
- "optional": true
21
- },
22
17
  "@nx/devkit": {
23
18
  "optional": true
24
- },
25
- "vite": {
26
- "optional": true
27
19
  }
28
20
  },
21
+ "ethlete": {
22
+ "migrations": "./migrations.json"
23
+ },
29
24
  "generators": "./generators/generators.json",
30
25
  "module": "fesm2022/ethlete-contentful.mjs",
31
26
  "typings": "types/ethlete-contentful.d.ts",
@@ -1,10 +1,11 @@
1
1
  import * as _angular_core from '@angular/core';
2
- import { Type, InputSignal, InjectionToken } from '@angular/core';
2
+ import { Type, InputSignal, InjectionToken, Provider } from '@angular/core';
3
3
  import { NgClassType } from '@ethlete/core';
4
4
  import { GQL } from '@ethlete/query';
5
5
  import { Mark, BLOCKS, INLINES, NodeData, Block } from '@contentful/rich-text-types';
6
6
  import * as _ethlete_components from '@ethlete/components';
7
7
  import { PictureSource } from '@ethlete/components';
8
+ import * as _angular_router from '@angular/router';
8
9
 
9
10
  declare const GQL_FRAGMENT_CONTENTFUL_ASSET: GQL;
10
11
  type ContentfulGqlAsset = {
@@ -84,43 +85,6 @@ type ContentfulGqlCollectionFilterVariables<TCustomWhere extends Record<string,
84
85
  locale?: string;
85
86
  };
86
87
 
87
- type HtmlOpenRenderCommand = {
88
- kind: 'htmlOpen';
89
- nestingLevel: number;
90
- domPosition: number;
91
- index: number;
92
- attributes: Record<string, string>;
93
- tagName: keyof HTMLElementTagNameMap;
94
- id: string;
95
- };
96
- type HtmlCloseRenderCommand = {
97
- kind: 'htmlClose';
98
- nestingLevel: number;
99
- domPosition: number;
100
- index: number;
101
- tagName: keyof HTMLElementTagNameMap;
102
- id: string;
103
- };
104
- type TextRenderCommand = {
105
- kind: 'text';
106
- nestingLevel: number;
107
- domPosition: number;
108
- index: number;
109
- attributes: Record<string, string>;
110
- markTags: MarkTagName[];
111
- text: string;
112
- id: string;
113
- };
114
- type ComponentRenderCommand = {
115
- kind: 'component';
116
- nestingLevel: number;
117
- domPosition: number;
118
- index: number;
119
- component: Type<unknown>;
120
- inputs: Record<string, unknown>;
121
- id: string;
122
- };
123
- type RenderCommand = HtmlOpenRenderCommand | HtmlCloseRenderCommand | TextRenderCommand | ComponentRenderCommand;
124
88
  type MarkTagName = 'strong' | 'em' | 'u' | 'code' | 's' | 'sub' | 'sup';
125
89
  /**
126
90
  * Translates contentful text marks into the semantic html elements the marked text is
@@ -178,9 +142,9 @@ type CreateContentfulIncludeMapConfig = {
178
142
  declare const createContentfulIncludeMap: (config: CreateContentfulIncludeMapConfig) => ContentfulIncludeMap;
179
143
  declare class ContentfulRichTextRendererComponent {
180
144
  private viewContainerRef;
181
- private readonly renderer;
145
+ private renderer;
182
146
  private elementRef;
183
- private readonly config;
147
+ private config;
184
148
  /**
185
149
  * The contentful response gotten via their REST api.
186
150
  * @see https://www.contentful.com/developers/docs/references/content-delivery-api/#/reference/entries/entries-collection
@@ -191,24 +155,10 @@ declare class ContentfulRichTextRendererComponent {
191
155
  * @example "items[0].fields.html"
192
156
  */
193
157
  richTextPath: _angular_core.InputSignal<string>;
194
- /**
195
- * A cache for all executed commands that are not deleted.
196
- * This is used to keep track of all rendered elements and components.
197
- * The key is the render command ID.
198
- */
199
158
  private readonly executedCommandsCache;
200
- /**
201
- * A map of all includes in the contentful response.
202
- * This is useful for looking up assets and entries that are referenced in the rich text without having to use loops.
203
- */
204
- contentIncludesMap: _angular_core.Signal<ContentfulIncludeMap>;
205
- /**
206
- * The rich text data that should be rendered.
207
- */
208
- richTextData: _angular_core.Signal<RichTextResponse | null>;
209
- renderCommands: _angular_core.Signal<RenderCommand[]>;
210
- private renderCommandHistory;
211
- private previousRenderCommandMap;
159
+ private contentIncludesMap;
160
+ private richTextData;
161
+ private renderCommands;
212
162
  private renderInstructions;
213
163
  constructor();
214
164
  private createRenderCommands;
@@ -263,8 +213,8 @@ type ContentfulConfig = {
263
213
  /**
264
214
  * Additional hostnames that should be treated as internal links (in addition to the current page's host).
265
215
  * Useful when the app runs on localhost during development but Contentful content references the production domain.
266
- * Comparison is done by primary domain (last two hostname labels), so adding "example.com" is sufficient
267
- * to also treat "foo.example.com" as internal.
216
+ * A configured hostname also covers its subdomains, so adding "example.com" treats
217
+ * "foo.example.com" as internal without matching unrelated public-suffix siblings.
268
218
  *
269
219
  * @example ['example.com']
270
220
  */
@@ -278,7 +228,7 @@ type ContentfulConfig = {
278
228
  * - `"400"` - 400px width
279
229
  * - `"400x300"` - 400px width and 300px height
280
230
  * - `"400w"` - 400px width
281
- * - `"400h"` - 400px height
231
+ * - `"400h"` - 400px height (requires image dimensions to derive a valid width descriptor)
282
232
  * - `"400wx300h"` - 400px width and 300px height
283
233
  **/
284
234
  srcsetSizes: string[];
@@ -368,9 +318,9 @@ type ContentfulEntry<T = {
368
318
  metadata: ContentfulMetadata;
369
319
  };
370
320
  type ContentfulCollection = {
371
- includes: {
372
- Asset: ContentfulRestAsset[];
373
- Entry: ContentfulEntry[];
321
+ includes?: {
322
+ Asset?: ContentfulRestAsset[];
323
+ Entry?: ContentfulEntry[];
374
324
  };
375
325
  items: ContentfulEntry[];
376
326
  limit: number;
@@ -391,8 +341,8 @@ declare class ContentfulAudioComponent {
391
341
  audioClass: _angular_core.InputSignal<NgClassType>;
392
342
  figureClass: _angular_core.InputSignal<NgClassType>;
393
343
  figcaptionClass: _angular_core.InputSignal<NgClassType>;
394
- data: _angular_core.Signal<{
395
- url: string | null;
344
+ protected data: _angular_core.Signal<{
345
+ url: string;
396
346
  title: string | null;
397
347
  } | null>;
398
348
  static ɵfac: _angular_core.ɵɵFactoryDeclaration<ContentfulAudioComponent, never>;
@@ -402,9 +352,8 @@ declare class ContentfulAudioComponent {
402
352
  declare class ContentfulFileComponent {
403
353
  asset: _angular_core.InputSignal<ContentfulGqlAsset | ContentfulRestAsset | null | undefined>;
404
354
  fileClass: _angular_core.InputSignal<NgClassType>;
405
- data: _angular_core.Signal<{
406
- url: string | null;
407
- contentType: string | null;
355
+ protected data: _angular_core.Signal<{
356
+ url: string;
408
357
  size: number | null;
409
358
  title: string | null;
410
359
  } | null>;
@@ -451,14 +400,16 @@ declare const generateContentfulImageSources: (data: ContentfulRestAsset | Conte
451
400
 
452
401
  declare class ContentfulLinkComponent {
453
402
  private document;
454
- private readonly config;
403
+ private router;
404
+ private config;
455
405
  href: _angular_core.InputSignal<string>;
456
406
  text: _angular_core.InputSignal<string>;
457
407
  textClass: _angular_core.InputSignal<string>;
458
- isExternal: _angular_core.Signal<boolean>;
459
- openInNewTab: _angular_core.Signal<boolean>;
460
- internalPath: _angular_core.Signal<string>;
461
- linkClass: _angular_core.Signal<string>;
408
+ protected usesRouterLink: _angular_core.Signal<boolean>;
409
+ protected openInNewTab: _angular_core.Signal<boolean>;
410
+ protected internalPath: _angular_core.Signal<string>;
411
+ protected internalUrlTree: _angular_core.Signal<_angular_router.UrlTree>;
412
+ protected linkClass: _angular_core.Signal<string>;
462
413
  static ɵfac: _angular_core.ɵɵFactoryDeclaration<ContentfulLinkComponent, never>;
463
414
  static ɵcmp: _angular_core.ɵɵComponentDeclaration<ContentfulLinkComponent, "et-contentful-link", never, { "href": { "alias": "href"; "required": true; "isSignal": true; }; "text": { "alias": "text"; "required": true; "isSignal": true; }; "textClass": { "alias": "textClass"; "required": false; "isSignal": true; }; }, {}, never, never, true, never>;
464
415
  }
@@ -466,8 +417,8 @@ declare class ContentfulLinkComponent {
466
417
  declare class ContentfulVideoComponent {
467
418
  asset: _angular_core.InputSignal<ContentfulGqlAsset | ContentfulRestAsset | null | undefined>;
468
419
  videoClass: _angular_core.InputSignal<NgClassType>;
469
- data: _angular_core.Signal<{
470
- url: string | null;
420
+ protected data: _angular_core.Signal<{
421
+ url: string;
471
422
  contentType: string | null;
472
423
  } | null>;
473
424
  static ɵfac: _angular_core.ɵɵFactoryDeclaration<ContentfulVideoComponent, never>;
@@ -478,17 +429,14 @@ declare const CONTENTFUL_CONFIG: InjectionToken<ContentfulConfig>;
478
429
 
479
430
  declare const ContentfulImports: readonly [typeof ContentfulAudioComponent, typeof ContentfulFileComponent, typeof ContentfulImageComponent, typeof ContentfulVideoComponent, typeof ContentfulRichTextRendererComponent];
480
431
 
481
- declare const createContentfulConfig: (config?: Partial<ContentfulConfig> | null | undefined) => ContentfulConfig;
432
+ declare const createContentfulConfig: (config?: Partial<ContentfulConfig> | null) => ContentfulConfig;
482
433
  /**
483
434
  * Reads the contentful config from the current injector. Falls back to a config without
484
435
  * any `components` when none was provided via `provideContentfulConfig()`.
485
436
  */
486
437
  declare const injectContentfulConfig: () => ContentfulConfig;
487
438
 
488
- declare const provideContentfulConfig: (contentfulConfig?: Partial<ContentfulConfig> | null | undefined) => {
489
- provide: _angular_core.InjectionToken<ContentfulConfig>;
490
- useValue: ContentfulConfig;
491
- };
439
+ declare const provideContentfulConfig: (contentfulConfig?: Partial<ContentfulConfig> | null) => Provider;
492
440
  declare const isContentfulEntryType: <T extends ContentfulEntry>(entry: ContentfulEntry, type: string) => entry is T;
493
441
 
494
442
  export { CONTENTFUL_CONFIG, ContentfulAudioComponent, ContentfulFileComponent, ContentfulImageComponent, ContentfulImports, ContentfulLinkComponent, ContentfulRichTextRendererComponent, ContentfulVideoComponent, ET_CONTENTFUL_ANY_ENTRY_CONTENT_TYPE_SYS_ID, GQL_FRAGMENT_CONTENTFUL_ASSET, createContentfulConfig, createContentfulIncludeMap, generateContentfulImageSources, generateDefaultContentfulImageSource, injectContentfulConfig, isContentfulEntryType, isContentfulGqlAsset, isRichTextRootNode, marksToClass, marksToTags, parseContentfulImageSize, provideContentfulConfig, translateContentfulNodeTypeToHtmlTag };