@ethlete/contentful 4.0.0-next.11 → 4.0.0-next.13

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.
@@ -0,0 +1,163 @@
1
+ import { formatFiles } from '@nx/devkit';
2
+ import { createMigrationScope } from '../migrate-to-contentful-v5/migration-scope.js';
3
+ export const CONTENTFUL_ASSET_CLASSES_REPORT_PATH = 'contentful-asset-classes-migration-tasks.md';
4
+ const REMOVED_CLASS_INPUTS = {
5
+ 'et-contentful-video': { videoClass: 'et-contentful-video-video' },
6
+ 'et-contentful-audio': {
7
+ audioClass: 'et-contentful-audio-audio',
8
+ figureClass: 'et-contentful-audio-figure',
9
+ figcaptionClass: 'et-contentful-audio-figcaption',
10
+ },
11
+ 'et-contentful-file': { fileClass: 'et-contentful-file-anchor' },
12
+ 'et-contentful-link': { textClass: 'et-contentful-link-anchor', anchorClass: 'et-contentful-link-anchor' },
13
+ };
14
+ const TAG_REGEX = new RegExp(`<(${Object.keys(REMOVED_CLASS_INPUTS).join('|')})(?=[\\s/>])`, 'g');
15
+ const INLINE_TEMPLATE_REGEX = /template\s*:\s*`([\s\S]*?)`/g;
16
+ const LINK_INPUT_DECLARATION_REGEX = /\b(textClass|anchorClass)\s*=\s*input\b/g;
17
+ const lineOf = (source, index) => source.slice(0, index).split('\n').length;
18
+ const findTagEnd = (source, from) => {
19
+ let quote = null;
20
+ for (let index = from; index < source.length; index++) {
21
+ const character = source[index];
22
+ if (quote) {
23
+ if (character === quote)
24
+ quote = null;
25
+ }
26
+ else if (character === '"' || character === "'") {
27
+ quote = character;
28
+ }
29
+ else if (character === '>') {
30
+ return index;
31
+ }
32
+ }
33
+ return -1;
34
+ };
35
+ const isInsideQuotes = (tag, index) => {
36
+ let quote = null;
37
+ for (let position = 0; position < index; position++) {
38
+ const character = tag[position];
39
+ if (quote) {
40
+ if (character === quote)
41
+ quote = null;
42
+ }
43
+ else if (character === '"' || character === "'") {
44
+ quote = character;
45
+ }
46
+ }
47
+ return quote !== null;
48
+ };
49
+ const removeClassInputs = (template) => {
50
+ const removals = [];
51
+ let content = '';
52
+ let cursor = 0;
53
+ for (const tagMatch of template.matchAll(TAG_REGEX)) {
54
+ const start = tagMatch.index ?? 0;
55
+ if (start < cursor)
56
+ continue;
57
+ const end = findTagEnd(template, start);
58
+ if (end === -1)
59
+ break;
60
+ const tagName = tagMatch[1] ?? '';
61
+ const inputs = Object.keys(REMOVED_CLASS_INPUTS[tagName] ?? {});
62
+ const attributeRegex = new RegExp(`(\\s+)(\\[?(${inputs.join('|')})\\]?(?:\\s*=\\s*(?:"[^"]*"|'[^']*'))?)(?=[\\s/>])`, 'g');
63
+ const tag = template.slice(start, end + 1);
64
+ const attributeMatches = [...tag.matchAll(attributeRegex)].filter((attributeMatch) => !isInsideQuotes(tag, attributeMatch.index ?? 0));
65
+ let nextTag = tag;
66
+ for (const attributeMatch of [...attributeMatches].reverse()) {
67
+ const index = attributeMatch.index ?? 0;
68
+ nextTag = nextTag.slice(0, index) + nextTag.slice(index + attributeMatch[0].length);
69
+ }
70
+ for (const attributeMatch of attributeMatches) {
71
+ removals.push({
72
+ offset: start + (attributeMatch.index ?? 0) + (attributeMatch[1] ?? '').length,
73
+ tagName,
74
+ input: attributeMatch[3] ?? '',
75
+ attribute: attributeMatch[2] ?? '',
76
+ });
77
+ }
78
+ content += template.slice(cursor, start) + nextTag;
79
+ cursor = end + 1;
80
+ }
81
+ content += template.slice(cursor);
82
+ return { content, removals };
83
+ };
84
+ const createRemovalTask = (filePath, line, removal) => {
85
+ const replacement = REMOVED_CLASS_INPUTS[removal.tagName]?.[removal.input] ?? '';
86
+ return {
87
+ id: `asset-class-input:${filePath}:${line}:${removal.input}`,
88
+ file: filePath,
89
+ line,
90
+ message: `Removed \`${removal.attribute}\` from \`${removal.tagName}\`. Style the static \`.${replacement}\` class instead.`,
91
+ };
92
+ };
93
+ const migrateHtmlFile = (filePath, source) => {
94
+ const { content, removals } = removeClassInputs(source);
95
+ return {
96
+ content,
97
+ tasks: removals.map((removal) => createRemovalTask(filePath, lineOf(source, removal.offset), removal)),
98
+ };
99
+ };
100
+ const migrateTsFile = (filePath, source) => {
101
+ const tasks = [];
102
+ const content = source.replace(INLINE_TEMPLATE_REGEX, (match, template, offset) => {
103
+ const templateOffset = offset + match.indexOf('`') + 1;
104
+ const result = removeClassInputs(template);
105
+ for (const removal of result.removals) {
106
+ tasks.push(createRemovalTask(filePath, lineOf(source, templateOffset + removal.offset), removal));
107
+ }
108
+ return result.removals.length > 0 ? match.replace(template, () => result.content) : match;
109
+ });
110
+ for (const declaration of source.matchAll(LINK_INPUT_DECLARATION_REGEX)) {
111
+ const line = lineOf(source, declaration.index ?? 0);
112
+ tasks.push({
113
+ id: `link-class-input:${filePath}:${line}`,
114
+ file: filePath,
115
+ line,
116
+ message: `\`${declaration[1]}\` is declared as an input. If this is a custom \`components.link\`, the rich-text renderer no longer sets it - it passes \`marks\` (the shared mark types) and \`richText: true\` instead.`,
117
+ });
118
+ }
119
+ return { content, tasks };
120
+ };
121
+ const renderReport = (tasks) => [
122
+ '# Contentful asset class inputs migration tasks',
123
+ '',
124
+ 'The class inputs of `et-contentful-video`, `et-contentful-audio`, `et-contentful-file` and',
125
+ '`et-contentful-link` are gone. Every inner element carries a static `et-contentful-*` class instead.',
126
+ 'The migration removed the bindings below; move their styling onto the named class. Delete this file when done.',
127
+ '',
128
+ ...tasks.map((task) => [`## \`${task.id}\``, '', `- ${task.file}:${task.line}`, `- ${task.message}`, ''].join('\n')),
129
+ ].join('\n');
130
+ export default async function migrateContentfulAssetClasses(tree, schema) {
131
+ console.log('\n🔄 Removing the Contentful asset class inputs...');
132
+ const scope = createMigrationScope(tree, schema);
133
+ console.log(` Scope: ${scope.describe()}`);
134
+ const tasks = [];
135
+ let filesChanged = 0;
136
+ scope.visit(tree, (filePath) => {
137
+ const isHtml = filePath.endsWith('.html');
138
+ const isTs = filePath.endsWith('.ts') && !filePath.endsWith('.d.ts');
139
+ if (!isHtml && !isTs)
140
+ return;
141
+ const before = tree.read(filePath, 'utf-8');
142
+ if (!before)
143
+ return;
144
+ const result = isHtml ? migrateHtmlFile(filePath, before) : migrateTsFile(filePath, before);
145
+ tasks.push(...result.tasks);
146
+ if (result.content !== before) {
147
+ tree.write(filePath, result.content);
148
+ filesChanged++;
149
+ console.log(` ✓ ${filePath}`);
150
+ }
151
+ });
152
+ if (tasks.length > 0) {
153
+ tree.write(CONTENTFUL_ASSET_CLASSES_REPORT_PATH, renderReport(tasks));
154
+ }
155
+ if (!schema.skipFormat) {
156
+ await formatFiles(tree);
157
+ }
158
+ console.log(`\n✅ Updated ${filesChanged} file(s).`);
159
+ if (tasks.length > 0) {
160
+ console.log(`⚠️ ${tasks.length} site(s) need their styling moved — see ${CONTENTFUL_ASSET_CLASSES_REPORT_PATH}.`);
161
+ }
162
+ }
163
+ //# sourceMappingURL=migration.js.map
@@ -0,0 +1,26 @@
1
+ {
2
+ "$schema": "http://json-schema.org/schema",
3
+ "$id": "migrate-contentful-asset-classes",
4
+ "title": "Remove the Contentful asset and link class inputs",
5
+ "type": "object",
6
+ "properties": {
7
+ "projects": {
8
+ "type": "array",
9
+ "items": { "type": "string" },
10
+ "description": "Nx projects to migrate. Defaults to every project in the workspace. Every site is rewritten independently, so any scope is safe — use this to keep the diff reviewable.",
11
+ "alias": "project"
12
+ },
13
+ "include": {
14
+ "type": "array",
15
+ "items": { "type": "string" },
16
+ "description": "Path prefixes to migrate, relative to the workspace root (e.g. 'libs/ui'). Unioned with `projects`. Defaults to the whole workspace.",
17
+ "alias": "includePath"
18
+ },
19
+ "skipFormat": {
20
+ "type": "boolean",
21
+ "description": "Skip formatting files after migration",
22
+ "default": false
23
+ }
24
+ },
25
+ "required": []
26
+ }
@@ -0,0 +1,73 @@
1
+ const CONSTANT = 'CONTENTFUL_DEFAULT_COMPONENTS';
2
+ const SPREAD = `...${CONSTANT}`;
3
+ const CONTENTFUL_IMPORT = /import\s*\{([^}]*)\}\s*from\s*['"]@ethlete\/contentful['"]/g;
4
+ const lineOf = (content, index) => content.slice(0, index).split('\n').length;
5
+ const taskId = (filePath, line) => `contentful-default-components--${filePath.replace(/[^a-zA-Z0-9]+/g, '-')}-${line}`;
6
+ const namesOwnComponents = (content, braceIndex) => {
7
+ let depth = 0;
8
+ let topLevel = '';
9
+ for (let i = braceIndex; i < content.length; i++) {
10
+ const char = content[i] ?? '';
11
+ if ('{[('.includes(char))
12
+ depth++;
13
+ else if ('}])'.includes(char))
14
+ depth--;
15
+ else if (depth === 1)
16
+ topLevel += char;
17
+ if (depth === 0)
18
+ break;
19
+ }
20
+ return /(^|[,\s])components\s*:/.test(topLevel);
21
+ };
22
+ const contentfulImportNaming = (content, name) => [...content.matchAll(CONTENTFUL_IMPORT)].find((match) => name.test(match[1] ?? ''));
23
+ export const importsProvideContentfulConfig = (content) => contentfulImportNaming(content, /\bprovideContentfulConfig\b/) !== undefined;
24
+ export const findUnprovidedRendererTask = (filePath, content) => {
25
+ const match = contentfulImportNaming(content, /\b(ContentfulRichTextRendererComponent|ContentfulImports)\b/);
26
+ if (!match)
27
+ return null;
28
+ const line = lineOf(content, match.index ?? 0);
29
+ return {
30
+ id: taskId(filePath, line),
31
+ file: filePath,
32
+ line,
33
+ message: `Renders Contentful rich text, but no file calls \`provideContentfulConfig\`. Without it embedded assets are skipped and links lose router navigation. Add \`provideContentfulConfig({ ${SPREAD} })\` to the application providers.`,
34
+ };
35
+ };
36
+ export const scanContentfulDefaultComponentsInFile = (filePath, content) => {
37
+ const tasks = [];
38
+ const importStatement = contentfulImportNaming(content, /\bprovideContentfulConfig\b/);
39
+ if (!importStatement || content.includes(CONSTANT))
40
+ return { next: null, tasks };
41
+ const report = (index, message) => {
42
+ const line = lineOf(content, index);
43
+ tasks.push({ id: taskId(filePath, line), file: filePath, line, message });
44
+ };
45
+ let rewrote = false;
46
+ const next = content.replace(/provideContentfulConfig\(\s*(\{\s*\}|\{\s*|\)|[^\s{)])/g, (call, opening, index) => {
47
+ if (opening === ')') {
48
+ rewrote = true;
49
+ return `provideContentfulConfig({ ${SPREAD} })`;
50
+ }
51
+ if (opening.startsWith('{')) {
52
+ rewrote = true;
53
+ if (namesOwnComponents(content, index + call.length - opening.length)) {
54
+ report(index, `\`provideContentfulConfig\` names its own \`components\`, which replaces the spread. Write \`components: { ...${CONSTANT}.components, ... }\` to keep the defaults.`);
55
+ }
56
+ return opening.endsWith('}')
57
+ ? `provideContentfulConfig({ ${SPREAD} }`
58
+ : `provideContentfulConfig({ ${SPREAD}, `;
59
+ }
60
+ const argument = content.slice(index + call.length - 1).match(/^[^)\n]*/)?.[0] ?? '';
61
+ report(index, `\`provideContentfulConfig(${argument})\` is not a literal the migration can edit. Spread \`${CONSTANT}\` into it unless it names every asset and link component itself.`);
62
+ return call;
63
+ });
64
+ if (!rewrote)
65
+ return { next: null, tasks };
66
+ const rewrittenImport = importStatement[0].replace('{', `{ ${CONSTANT},`);
67
+ const importIndex = next.indexOf(importStatement[0]);
68
+ return {
69
+ next: next.slice(0, importIndex) + rewrittenImport + next.slice(importIndex + importStatement[0].length),
70
+ tasks,
71
+ };
72
+ };
73
+ //# sourceMappingURL=contentful-default-components.js.map
@@ -0,0 +1,74 @@
1
+ import { formatFiles, visitNotIgnoredFiles } from '@nx/devkit';
2
+ import { createMigrationScope } from '../migrate-to-contentful-v5/migration-scope.js';
3
+ import { findUnprovidedRendererTask, importsProvideContentfulConfig, scanContentfulDefaultComponentsInFile, } from './contentful-default-components.js';
4
+ export const CONTENTFUL_DEFAULT_COMPONENTS_REPORT_PATH = 'contentful-default-components-migration-tasks.md';
5
+ const isSourceFile = (filePath) => filePath.endsWith('.ts') && !filePath.endsWith('.d.ts');
6
+ const workspaceProvidesContentfulConfig = (tree) => {
7
+ let found = false;
8
+ visitNotIgnoredFiles(tree, '', (filePath) => {
9
+ if (found || !isSourceFile(filePath))
10
+ return;
11
+ found = importsProvideContentfulConfig(tree.read(filePath, 'utf-8') ?? '');
12
+ });
13
+ return found;
14
+ };
15
+ const renderReport = (tasks) => [
16
+ '# Contentful default components migration tasks',
17
+ '',
18
+ 'The shipped asset and link components are opt-in. Without them embedded assets are skipped and',
19
+ 'hyperlinks render as plain anchors. The migration spread `CONTENTFUL_DEFAULT_COMPONENTS` into every',
20
+ 'literal `provideContentfulConfig` call; the places below need it by hand:',
21
+ '',
22
+ '```ts',
23
+ "import { CONTENTFUL_DEFAULT_COMPONENTS, provideContentfulConfig } from '@ethlete/contentful';",
24
+ '',
25
+ 'provideContentfulConfig({ ...CONTENTFUL_DEFAULT_COMPONENTS });',
26
+ '```',
27
+ '',
28
+ 'A config that names every asset and link component itself needs nothing. Delete this file when done.',
29
+ '',
30
+ ...tasks.map((task) => [`## \`${task.id}\``, '', `- ${task.file}:${task.line}`, `- ${task.message}`, ''].join('\n')),
31
+ ].join('\n');
32
+ export default async function migrateContentfulDefaultComponents(tree, schema) {
33
+ console.log('\n🔎 Spreading CONTENTFUL_DEFAULT_COMPONENTS into provideContentfulConfig...');
34
+ const scope = createMigrationScope(tree, schema);
35
+ console.log(` Scope: ${scope.describe()}`);
36
+ const changed = [];
37
+ const tasks = [];
38
+ const rendererTasks = [];
39
+ scope.visit(tree, (filePath) => {
40
+ if (!isSourceFile(filePath))
41
+ return;
42
+ const content = tree.read(filePath, 'utf-8');
43
+ if (!content)
44
+ return;
45
+ const result = scanContentfulDefaultComponentsInFile(filePath, content);
46
+ const rendererTask = findUnprovidedRendererTask(filePath, content);
47
+ tasks.push(...result.tasks);
48
+ if (rendererTask)
49
+ rendererTasks.push(rendererTask);
50
+ if (result.next !== null) {
51
+ tree.write(filePath, result.next);
52
+ changed.push(filePath);
53
+ }
54
+ });
55
+ if (rendererTasks.length > 0 && !workspaceProvidesContentfulConfig(tree)) {
56
+ tasks.push(...rendererTasks);
57
+ }
58
+ if (tasks.length > 0) {
59
+ tree.write(CONTENTFUL_DEFAULT_COMPONENTS_REPORT_PATH, renderReport(tasks));
60
+ }
61
+ if (!schema.skipFormat) {
62
+ await formatFiles(tree);
63
+ }
64
+ if (changed.length > 0) {
65
+ console.log(`\n✅ Spread CONTENTFUL_DEFAULT_COMPONENTS in ${changed.length} file(s).`);
66
+ }
67
+ if (tasks.length > 0) {
68
+ console.log(`\n⚠️ ${tasks.length} place(s) need the default components by hand — see ${CONTENTFUL_DEFAULT_COMPONENTS_REPORT_PATH}.`);
69
+ }
70
+ else if (changed.length === 0) {
71
+ console.log('\n✅ Nothing to do.');
72
+ }
73
+ }
74
+ //# sourceMappingURL=migration.js.map
@@ -0,0 +1,26 @@
1
+ {
2
+ "$schema": "http://json-schema.org/schema",
3
+ "$id": "migrate-contentful-default-components",
4
+ "title": "Spread CONTENTFUL_DEFAULT_COMPONENTS into provideContentfulConfig",
5
+ "type": "object",
6
+ "properties": {
7
+ "projects": {
8
+ "type": "array",
9
+ "items": { "type": "string" },
10
+ "description": "Nx projects to migrate. Defaults to every project in the workspace. Every site is rewritten independently, so any scope is safe — use this to keep the diff reviewable.",
11
+ "alias": "project"
12
+ },
13
+ "include": {
14
+ "type": "array",
15
+ "items": { "type": "string" },
16
+ "description": "Path prefixes to migrate, relative to the workspace root (e.g. 'libs/ui'). Unioned with `projects`. Defaults to the whole workspace.",
17
+ "alias": "includePath"
18
+ },
19
+ "skipFormat": {
20
+ "type": "boolean",
21
+ "description": "Skip formatting files after migration",
22
+ "default": false
23
+ }
24
+ },
25
+ "required": []
26
+ }
@@ -3,7 +3,7 @@ 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';
5
5
  const COMPONENTS_VERSION = '^1.0.0-next.32';
6
- /** Inputs that v5 dropped — the picture element now only carries static `et-picture-*` classes. */
6
+ /** Inputs that 4.0 dropped — the picture element now only carries static `et-picture-*` classes. */
7
7
  const REMOVED_CLASS_INPUTS = ['imgClass', 'figureClass', 'figcaptionClass', 'pictureClass'];
8
8
  /** Renderer internals that are no longer exported from `@ethlete/contentful`. */
9
9
  const REMOVED_EXPORTS = [
@@ -188,7 +188,7 @@ const migratePackageJson = (tree, filePath) => {
188
188
  return { changed, tasks };
189
189
  };
190
190
  const renderReport = (tasks) => [
191
- '# @ethlete/contentful v5 migration tasks',
191
+ '# @ethlete/contentful 4 migration tasks',
192
192
  '',
193
193
  'The codemod renamed the `et-contentful-image` `hasPriority` input to `priority`, removed the dropped',
194
194
  '`useTailwindClasses` config option and made sure `@ethlete/components` is declared. The sites below',
@@ -205,7 +205,7 @@ const renderReport = (tasks) => [
205
205
  ...tasks.map((task) => [`## \`${task.id}\``, '', `- ${task.file}${task.line ? `:${task.line}` : ''}`, `- ${task.message}`, ''].join('\n')),
206
206
  ].join('\n');
207
207
  export default async function migrateToContentfulV5(tree, schema) {
208
- console.log('\n🔄 Migrating to @ethlete/contentful v5...');
208
+ console.log('\n🔄 Migrating to @ethlete/contentful 4...');
209
209
  const scope = createMigrationScope(tree, schema);
210
210
  console.log(` Scope: ${scope.describe()}`);
211
211
  const tasks = [];
package/migrations.json CHANGED
@@ -4,9 +4,28 @@
4
4
  "name": "to-contentful-v5",
5
5
  "version": "4.0.0-next.7",
6
6
  "kind": "auto",
7
+ "level": "required",
7
8
  "description": "Move to the components picture, the renamed image inputs and the removed renderer internals.",
8
9
  "generator": "@ethlete/contentful:migrate-to-contentful-v5",
9
10
  "docs": "/contentful/"
11
+ },
12
+ {
13
+ "name": "contentful-default-components",
14
+ "version": "4.0.0-next.12",
15
+ "kind": "auto",
16
+ "level": "required",
17
+ "description": "Spread CONTENTFUL_DEFAULT_COMPONENTS into provideContentfulConfig, and report the calls that need it by hand.",
18
+ "generator": "@ethlete/contentful:migrate-contentful-default-components",
19
+ "docs": "/contentful/"
20
+ },
21
+ {
22
+ "name": "contentful-asset-classes",
23
+ "version": "4.0.0-next.13",
24
+ "kind": "auto",
25
+ "level": "required",
26
+ "description": "Remove the dropped video, audio, file and link class inputs, and report each removal with the static class that replaces it.",
27
+ "generator": "@ethlete/contentful:migrate-contentful-asset-classes",
28
+ "docs": "/contentful/"
10
29
  }
11
30
  ]
12
31
  }
package/package.json CHANGED
@@ -1,17 +1,17 @@
1
1
  {
2
2
  "name": "@ethlete/contentful",
3
- "version": "4.0.0-next.11",
3
+ "version": "4.0.0-next.13",
4
4
  "license": "MIT",
5
5
  "sideEffects": false,
6
6
  "peerDependencies": {
7
- "@angular/common": "22.1.6",
8
- "@angular/core": "22.1.6",
9
- "@angular/router": "22.1.6",
10
- "@contentful/rich-text-types": "17.2.7",
7
+ "@angular/common": "^22.1.0",
8
+ "@angular/core": "^22.1.0",
9
+ "@angular/router": "^22.1.0",
10
+ "@contentful/rich-text-types": "^17.0.0",
11
11
  "@ethlete/components": "^1.0.0-next.32",
12
12
  "@ethlete/core": "^5.0.0-beta.11",
13
13
  "@ethlete/query": "^6.0.0-beta.8",
14
- "@nx/devkit": "23.1.0"
14
+ "@nx/devkit": "^23.0.0"
15
15
  },
16
16
  "peerDependenciesMeta": {
17
17
  "@nx/devkit": {