@ethlete/contentful 4.0.0-next.1 → 4.0.0-next.10

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,256 @@
1
+ import { formatFiles, installPackagesTask, readJson, updateJson } from '@nx/devkit';
2
+ import { createMigrationScope } from './migration-scope.js';
3
+ export const CONTENTFUL_V5_REPORT_PATH = 'contentful-v5-migration-tasks.md';
4
+ const COMPONENTS_PACKAGE = '@ethlete/components';
5
+ const COMPONENTS_VERSION = '^1.0.0-next.32';
6
+ /** Inputs that v5 dropped — the picture element now only carries static `et-picture-*` classes. */
7
+ const REMOVED_CLASS_INPUTS = ['imgClass', 'figureClass', 'figcaptionClass', 'pictureClass'];
8
+ /** Renderer internals that are no longer exported from `@ethlete/contentful`. */
9
+ const REMOVED_EXPORTS = [
10
+ 'RENDER_COMMAND_TYPE',
11
+ 'RENDER_COMMAND_POSITION',
12
+ 'HTML_OPEN_RENDER_COMMAND_POSITION',
13
+ 'HTML_CLOSE_RENDER_COMMAND_POSITION',
14
+ 'TEXT_RENDER_COMMAND_POSITION',
15
+ 'COMPONENT_RENDER_COMMAND_POSITION',
16
+ 'RENDER_INSTRUCTION_POSITION',
17
+ 'RENDER_INSTRUCTION_TYPE',
18
+ 'isHtmlOpenRenderCommand',
19
+ 'isHtmlCloseRenderCommand',
20
+ 'isTextRenderCommand',
21
+ 'isComponentRenderCommand',
22
+ 'getRenderCommandId',
23
+ 'isExecutedComponentCommandCacheItem',
24
+ 'isExecutedHtmlOrTextCommandCacheItem',
25
+ 'isExecutedTextCommandCacheItem',
26
+ ];
27
+ const INLINE_TEMPLATE_REGEX = /template\s*:\s*`([\s\S]*?)`/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
+ 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
+ };
71
+ /** Renames `hasPriority` to `priority` and collects removed class inputs, on `et-contentful-image` only. */
72
+ const migrateTemplate = (template) => {
73
+ let changed = false;
74
+ const classInputHits = [];
75
+ const content = replaceImageTags(template, (tag, offset) => {
76
+ const foundInputs = REMOVED_CLASS_INPUTS.filter((input) => new RegExp(`(^|[\\s[])${input}(\\]|=|(?=[\\s/>]))`).test(tag));
77
+ if (foundInputs.length > 0) {
78
+ classInputHits.push({ line: lineOf(template, offset), inputs: foundInputs });
79
+ }
80
+ const next = tag
81
+ .replace(/\[hasPriority\]=/g, '[priority]=')
82
+ .replace(/(^|\s)hasPriority(=|(?=[\s/>]))/g, '$1priority$2');
83
+ if (next !== tag)
84
+ changed = true;
85
+ return next;
86
+ });
87
+ return { content, changed, classInputHits };
88
+ };
89
+ const migrateHtmlFile = (filePath, source) => {
90
+ const { content, changed, classInputHits } = migrateTemplate(source);
91
+ return { content, changed, tasks: classInputHits.map((hit) => createClassInputTask(filePath, hit)) };
92
+ };
93
+ const createClassInputTask = (filePath, hit) => ({
94
+ id: `image-class-input:${filePath}:${hit.line}`,
95
+ file: filePath,
96
+ line: hit.line,
97
+ message: `\`et-contentful-image\` still sets ${hit.inputs
98
+ .map((input) => `\`${input}\``)
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.`,
100
+ });
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;
102
+ const migrateTsFile = (filePath, source) => {
103
+ const tasks = [];
104
+ let changed = false;
105
+ source = source.replace(USE_TAILWIND_CLASSES_REGEX, () => {
106
+ changed = true;
107
+ return '';
108
+ });
109
+ const content = source.replace(INLINE_TEMPLATE_REGEX, (match, template, offset) => {
110
+ const result = migrateTemplate(template);
111
+ const templateStartLine = lineOf(source, offset + match.indexOf('`'));
112
+ for (const hit of result.classInputHits) {
113
+ tasks.push(createClassInputTask(filePath, { ...hit, line: templateStartLine + hit.line - 1 }));
114
+ }
115
+ if (!result.changed)
116
+ return match;
117
+ changed = true;
118
+ return match.replace(template, () => result.content);
119
+ });
120
+ for (const importMatch of source.matchAll(CONTENTFUL_NAMED_REFERENCE_REGEX)) {
121
+ const symbols = (importMatch[1] ?? '')
122
+ .split(',')
123
+ .map((specifier) => specifier
124
+ .trim()
125
+ .replace(/^type\s+/, '')
126
+ .split(/\s+as\s+/)[0]
127
+ ?.trim() ?? '')
128
+ .filter(Boolean);
129
+ for (const symbol of symbols) {
130
+ if (!REMOVED_EXPORTS.includes(symbol))
131
+ continue;
132
+ tasks.push({
133
+ id: `removed-export:${filePath}:${symbol}`,
134
+ file: filePath,
135
+ line: lineOf(source, importMatch.index ?? 0),
136
+ message: `\`${symbol}\` is no longer public API. The render pipeline moved to plain objects internally, so the renderer command helpers are gone. Remove the usage, or vendor the logic into your own code.`,
137
+ });
138
+ }
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
+ }
149
+ return { content, changed, tasks };
150
+ };
151
+ const migratePackageJson = (tree, filePath) => {
152
+ const tasks = [];
153
+ let changed = false;
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
+ }
188
+ return { changed, tasks };
189
+ };
190
+ const renderReport = (tasks) => [
191
+ '# @ethlete/contentful v5 migration tasks',
192
+ '',
193
+ 'The codemod renamed the `et-contentful-image` `hasPriority` input to `priority`, removed the dropped',
194
+ '`useTailwindClasses` config option and made sure `@ethlete/components` is declared. The sites below',
195
+ 'need a decision a codemod cannot make.',
196
+ '',
197
+ 'Recipes:',
198
+ '',
199
+ '- Removed class inputs (`imgClass`, `figureClass`, `figcaptionClass`, `pictureClass`): the picture markup',
200
+ ' now carries static `et-picture-*` classes. Delete the binding and style those classes from your CSS.',
201
+ '- Removed renderer internals: the render pipeline is plain objects internally now. Drop the usage, or copy',
202
+ ' the helper into your own codebase.',
203
+ '- A leftover `@ethlete/cdk` dependency: remove it unless your own code imports it.',
204
+ '',
205
+ ...tasks.map((task) => [`## \`${task.id}\``, '', `- ${task.file}${task.line ? `:${task.line}` : ''}`, `- ${task.message}`, ''].join('\n')),
206
+ ].join('\n');
207
+ export default async function migrateToContentfulV5(tree, schema) {
208
+ console.log('\n🔄 Migrating to @ethlete/contentful v5...');
209
+ const scope = createMigrationScope(tree, schema);
210
+ console.log(` Scope: ${scope.describe()}`);
211
+ const tasks = [];
212
+ let filesChanged = 0;
213
+ let dependenciesChanged = false;
214
+ scope.visit(tree, (filePath) => {
215
+ if (filePath === CONTENTFUL_V5_REPORT_PATH)
216
+ return;
217
+ if (filePath.endsWith('package.json')) {
218
+ const result = migratePackageJson(tree, filePath);
219
+ tasks.push(...result.tasks);
220
+ if (result.changed)
221
+ filesChanged++;
222
+ if (result.changed)
223
+ dependenciesChanged = true;
224
+ return;
225
+ }
226
+ const isHtml = filePath.endsWith('.html');
227
+ const isTs = filePath.endsWith('.ts') && !filePath.endsWith('.d.ts');
228
+ if (!isHtml && !isTs)
229
+ return;
230
+ const before = tree.read(filePath, 'utf-8');
231
+ if (!before)
232
+ return;
233
+ const result = isHtml ? migrateHtmlFile(filePath, before) : migrateTsFile(filePath, before);
234
+ tasks.push(...result.tasks);
235
+ if (result.changed) {
236
+ tree.write(filePath, result.content);
237
+ filesChanged++;
238
+ console.log(` ✓ ${filePath}`);
239
+ }
240
+ });
241
+ if (tasks.length > 0) {
242
+ tree.write(CONTENTFUL_V5_REPORT_PATH, renderReport(tasks));
243
+ }
244
+ if (!schema.skipFormat) {
245
+ await formatFiles(tree);
246
+ }
247
+ console.log(`\n✅ Updated ${filesChanged} file(s).`);
248
+ if (tasks.length > 0) {
249
+ console.log(`⚠️ ${tasks.length} site(s) need a manual decision — see ${CONTENTFUL_V5_REPORT_PATH}.`);
250
+ }
251
+ if (dependenciesChanged) {
252
+ return (() => installPackagesTask(tree, true));
253
+ }
254
+ return undefined;
255
+ }
256
+ //# sourceMappingURL=migration.js.map
@@ -0,0 +1,26 @@
1
+ {
2
+ "$schema": "http://json-schema.org/schema",
3
+ "$id": "migrate-to-contentful-v5",
4
+ "title": "Migrate to @ethlete/contentful v5",
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,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,22 +1,27 @@
1
1
  {
2
2
  "name": "@ethlete/contentful",
3
- "version": "4.0.0-next.1",
3
+ "version": "4.0.0-next.10",
4
4
  "license": "MIT",
5
5
  "sideEffects": false,
6
6
  "peerDependencies": {
7
- "@analogjs/vite-plugin-angular": "2.3.1",
8
- "@angular/cdk": "21.2.1",
9
- "@angular/common": "21.2.1",
10
- "@angular/core": "21.2.1",
11
- "@angular/router": "21.2.1",
12
- "@contentful/rich-text-types": "17.2.5",
13
- "@ethlete/cdk": "^5.0.0-next.0",
14
- "@ethlete/core": "^5.0.0-next.0",
15
- "@ethlete/query": "^6.0.0-next.0",
16
- "@nx/vite": "22.5.4",
17
- "rxjs": "7.8.2",
18
- "vite": "^7.0.0"
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",
11
+ "@ethlete/components": "^1.0.0-next.32",
12
+ "@ethlete/core": "^5.0.0-beta.11",
13
+ "@ethlete/query": "^6.0.0-beta.8",
14
+ "@nx/devkit": "23.1.0"
19
15
  },
16
+ "peerDependenciesMeta": {
17
+ "@nx/devkit": {
18
+ "optional": true
19
+ }
20
+ },
21
+ "ethlete": {
22
+ "migrations": "./migrations.json"
23
+ },
24
+ "generators": "./generators/generators.json",
20
25
  "module": "fesm2022/ethlete-contentful.mjs",
21
26
  "typings": "types/ethlete-contentful.d.ts",
22
27
  "exports": {
@@ -28,6 +33,7 @@
28
33
  "default": "./fesm2022/ethlete-contentful.mjs"
29
34
  }
30
35
  },
36
+ "type": "module",
31
37
  "dependencies": {
32
38
  "tslib": "^2.3.0"
33
39
  }