@openrewrite/recipes-angular 1.1.3 → 1.1.5

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 (124) hide show
  1. package/dist/index.d.ts +18 -0
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +76 -1
  4. package/dist/index.js.map +1 -1
  5. package/dist/libraries/primeng/18/add-primeng-provider.d.ts +45 -0
  6. package/dist/libraries/primeng/18/add-primeng-provider.d.ts.map +1 -0
  7. package/dist/libraries/primeng/18/add-primeng-provider.js +354 -0
  8. package/dist/libraries/primeng/18/add-primeng-provider.js.map +1 -0
  9. package/dist/libraries/primeng/18/index.d.ts +19 -0
  10. package/dist/libraries/primeng/18/index.d.ts.map +1 -0
  11. package/dist/libraries/primeng/18/index.js +45 -0
  12. package/dist/libraries/primeng/18/index.js.map +1 -0
  13. package/dist/libraries/primeng/18/manual-migration-steps.d.ts +19 -0
  14. package/dist/libraries/primeng/18/manual-migration-steps.d.ts.map +1 -0
  15. package/dist/libraries/primeng/18/manual-migration-steps.js +45 -0
  16. package/dist/libraries/primeng/18/manual-migration-steps.js.map +1 -0
  17. package/dist/libraries/primeng/18/mark-deprecated-components.d.ts +9 -0
  18. package/dist/libraries/primeng/18/mark-deprecated-components.d.ts.map +1 -0
  19. package/dist/libraries/primeng/18/mark-deprecated-components.js +125 -0
  20. package/dist/libraries/primeng/18/mark-deprecated-components.js.map +1 -0
  21. package/dist/libraries/primeng/18/mark-deprecated-css-classes.d.ts +15 -0
  22. package/dist/libraries/primeng/18/mark-deprecated-css-classes.d.ts.map +1 -0
  23. package/dist/libraries/primeng/18/mark-deprecated-css-classes.js +108 -0
  24. package/dist/libraries/primeng/18/mark-deprecated-css-classes.js.map +1 -0
  25. package/dist/libraries/primeng/18/mark-drawer-size.d.ts +16 -0
  26. package/dist/libraries/primeng/18/mark-drawer-size.d.ts.map +1 -0
  27. package/dist/libraries/primeng/18/mark-drawer-size.js +90 -0
  28. package/dist/libraries/primeng/18/mark-drawer-size.js.map +1 -0
  29. package/dist/libraries/primeng/18/mark-removed-primeng-modules.d.ts +9 -0
  30. package/dist/libraries/primeng/18/mark-removed-primeng-modules.d.ts.map +1 -0
  31. package/dist/libraries/primeng/18/mark-removed-primeng-modules.js +186 -0
  32. package/dist/libraries/primeng/18/mark-removed-primeng-modules.js.map +1 -0
  33. package/dist/libraries/primeng/18/migrate-messages-to-message-loop.d.ts +15 -0
  34. package/dist/libraries/primeng/18/migrate-messages-to-message-loop.d.ts.map +1 -0
  35. package/dist/libraries/primeng/18/migrate-messages-to-message-loop.js +78 -0
  36. package/dist/libraries/primeng/18/migrate-messages-to-message-loop.js.map +1 -0
  37. package/dist/libraries/primeng/18/migrate-p-fluid-to-wrapper.d.ts +27 -0
  38. package/dist/libraries/primeng/18/migrate-p-fluid-to-wrapper.d.ts.map +1 -0
  39. package/dist/libraries/primeng/18/migrate-p-fluid-to-wrapper.js +495 -0
  40. package/dist/libraries/primeng/18/migrate-p-fluid-to-wrapper.js.map +1 -0
  41. package/dist/libraries/primeng/18/migrate-primeng-config.d.ts +8 -0
  42. package/dist/libraries/primeng/18/migrate-primeng-config.d.ts.map +1 -0
  43. package/dist/libraries/primeng/18/migrate-primeng-config.js +154 -0
  44. package/dist/libraries/primeng/18/migrate-primeng-config.js.map +1 -0
  45. package/dist/libraries/primeng/18/migrate-primeng-signal-assignments.d.ts +12 -0
  46. package/dist/libraries/primeng/18/migrate-primeng-signal-assignments.d.ts.map +1 -0
  47. package/dist/libraries/primeng/18/migrate-primeng-signal-assignments.js +112 -0
  48. package/dist/libraries/primeng/18/migrate-primeng-signal-assignments.js.map +1 -0
  49. package/dist/libraries/primeng/18/rename-calendar-to-datepicker.d.ts +8 -0
  50. package/dist/libraries/primeng/18/rename-calendar-to-datepicker.d.ts.map +1 -0
  51. package/dist/libraries/primeng/18/rename-calendar-to-datepicker.js +114 -0
  52. package/dist/libraries/primeng/18/rename-calendar-to-datepicker.js.map +1 -0
  53. package/dist/libraries/primeng/18/rename-dropdown-to-select.d.ts +8 -0
  54. package/dist/libraries/primeng/18/rename-dropdown-to-select.d.ts.map +1 -0
  55. package/dist/libraries/primeng/18/rename-dropdown-to-select.js +114 -0
  56. package/dist/libraries/primeng/18/rename-dropdown-to-select.js.map +1 -0
  57. package/dist/libraries/primeng/18/rename-inputswitch-to-toggleswitch.d.ts +8 -0
  58. package/dist/libraries/primeng/18/rename-inputswitch-to-toggleswitch.d.ts.map +1 -0
  59. package/dist/libraries/primeng/18/rename-inputswitch-to-toggleswitch.js +114 -0
  60. package/dist/libraries/primeng/18/rename-inputswitch-to-toggleswitch.js.map +1 -0
  61. package/dist/libraries/primeng/18/rename-message-interface.d.ts +8 -0
  62. package/dist/libraries/primeng/18/rename-message-interface.d.ts.map +1 -0
  63. package/dist/libraries/primeng/18/rename-message-interface.js +107 -0
  64. package/dist/libraries/primeng/18/rename-message-interface.js.map +1 -0
  65. package/dist/libraries/primeng/18/rename-overlaypanel-to-popover.d.ts +8 -0
  66. package/dist/libraries/primeng/18/rename-overlaypanel-to-popover.d.ts.map +1 -0
  67. package/dist/libraries/primeng/18/rename-overlaypanel-to-popover.js +114 -0
  68. package/dist/libraries/primeng/18/rename-overlaypanel-to-popover.js.map +1 -0
  69. package/dist/libraries/primeng/18/rename-sidebar-to-drawer.d.ts +8 -0
  70. package/dist/libraries/primeng/18/rename-sidebar-to-drawer.d.ts.map +1 -0
  71. package/dist/libraries/primeng/18/rename-sidebar-to-drawer.js +114 -0
  72. package/dist/libraries/primeng/18/rename-sidebar-to-drawer.js.map +1 -0
  73. package/dist/libraries/primeng/18/rename-template-selectors.d.ts +12 -0
  74. package/dist/libraries/primeng/18/rename-template-selectors.d.ts.map +1 -0
  75. package/dist/libraries/primeng/18/rename-template-selectors.js +79 -0
  76. package/dist/libraries/primeng/18/rename-template-selectors.js.map +1 -0
  77. package/dist/libraries/primeng/18/upgrade-components-to-18.d.ts +8 -0
  78. package/dist/libraries/primeng/18/upgrade-components-to-18.d.ts.map +1 -0
  79. package/dist/libraries/primeng/18/upgrade-components-to-18.js +73 -0
  80. package/dist/libraries/primeng/18/upgrade-components-to-18.js.map +1 -0
  81. package/dist/libraries/primeng/18/upgrade-to-primeng-18.d.ts +8 -0
  82. package/dist/libraries/primeng/18/upgrade-to-primeng-18.d.ts.map +1 -0
  83. package/dist/libraries/primeng/18/upgrade-to-primeng-18.js +68 -0
  84. package/dist/libraries/primeng/18/upgrade-to-primeng-18.js.map +1 -0
  85. package/dist/migration/explicit-standalone-flag.d.ts.map +1 -1
  86. package/dist/migration/explicit-standalone-flag.js +12 -5
  87. package/dist/migration/explicit-standalone-flag.js.map +1 -1
  88. package/dist/migration/replace-untyped-forms.d.ts +14 -2
  89. package/dist/migration/replace-untyped-forms.d.ts.map +1 -1
  90. package/dist/migration/replace-untyped-forms.js +108 -36
  91. package/dist/migration/replace-untyped-forms.js.map +1 -1
  92. package/dist/migration/upgrade-angular-peer-libs-to-18.d.ts +17 -0
  93. package/dist/migration/upgrade-angular-peer-libs-to-18.d.ts.map +1 -0
  94. package/dist/migration/upgrade-angular-peer-libs-to-18.js +49 -0
  95. package/dist/migration/upgrade-angular-peer-libs-to-18.js.map +1 -0
  96. package/dist/migration/upgrade-to-angular-18.d.ts.map +1 -1
  97. package/dist/migration/upgrade-to-angular-18.js +16 -7
  98. package/dist/migration/upgrade-to-angular-18.js.map +1 -1
  99. package/package.json +1 -1
  100. package/src/index.ts +57 -0
  101. package/src/libraries/primeng/18/add-primeng-provider.ts +359 -0
  102. package/src/libraries/primeng/18/index.ts +24 -0
  103. package/src/libraries/primeng/18/manual-migration-steps.ts +37 -0
  104. package/src/libraries/primeng/18/mark-deprecated-components.ts +128 -0
  105. package/src/libraries/primeng/18/mark-deprecated-css-classes.ts +109 -0
  106. package/src/libraries/primeng/18/mark-drawer-size.ts +85 -0
  107. package/src/libraries/primeng/18/mark-removed-primeng-modules.ts +198 -0
  108. package/src/libraries/primeng/18/migrate-messages-to-message-loop.ts +72 -0
  109. package/src/libraries/primeng/18/migrate-p-fluid-to-wrapper.ts +439 -0
  110. package/src/libraries/primeng/18/migrate-primeng-config.ts +112 -0
  111. package/src/libraries/primeng/18/migrate-primeng-signal-assignments.ts +83 -0
  112. package/src/libraries/primeng/18/rename-calendar-to-datepicker.ts +81 -0
  113. package/src/libraries/primeng/18/rename-dropdown-to-select.ts +81 -0
  114. package/src/libraries/primeng/18/rename-inputswitch-to-toggleswitch.ts +81 -0
  115. package/src/libraries/primeng/18/rename-message-interface.ts +78 -0
  116. package/src/libraries/primeng/18/rename-overlaypanel-to-popover.ts +81 -0
  117. package/src/libraries/primeng/18/rename-sidebar-to-drawer.ts +81 -0
  118. package/src/libraries/primeng/18/rename-template-selectors.ts +63 -0
  119. package/src/libraries/primeng/18/upgrade-components-to-18.ts +57 -0
  120. package/src/libraries/primeng/18/upgrade-to-primeng-18.ts +52 -0
  121. package/src/migration/explicit-standalone-flag.ts +11 -5
  122. package/src/migration/replace-untyped-forms.ts +111 -39
  123. package/src/migration/upgrade-angular-peer-libs-to-18.ts +33 -0
  124. package/src/migration/upgrade-to-angular-18.ts +16 -7
@@ -0,0 +1,439 @@
1
+ /*
2
+ * Copyright 2026 the original author or authors.
3
+ *
4
+ * Moderne Proprietary. Only for use by Moderne customers under the terms of a commercial contract.
5
+ */
6
+
7
+ import {ExecutionContext, Recipe, ScanningRecipe, TreeVisitor} from "@openrewrite/rewrite";
8
+ import {PlainText, PlainTextVisitor} from "@openrewrite/rewrite/text";
9
+ import {JavaScriptVisitor, JS, maybeAddImport} from "@openrewrite/rewrite/javascript";
10
+ import {J, isIdentifier} from "@openrewrite/rewrite/java";
11
+ import * as path from "path";
12
+
13
+ /**
14
+ * HTML files (repo-relative source paths) where the HTML sub-recipe rewrote `class="…p-fluid…"`
15
+ * to a `<p-fluid>` wrapper. The TS sub-recipe reads this set to decide which `*.component.ts`
16
+ * files need `FluidModule` added to their imports.
17
+ *
18
+ * Module-scoped because the two sub-recipes run sequentially within a single mod invocation —
19
+ * passing state via the recipe's `recipeList()` chain is the simplest way to share between the
20
+ * HTML and TS visitor types.
21
+ */
22
+ const rewrittenHtmlPaths: Set<string> = new Set();
23
+
24
+ /**
25
+ * Rewrites `<div class="…p-fluid…">…</div>` to `<p-fluid class="…">…</p-fluid>` in HTML
26
+ * templates. Uses scanner phase to pre-populate `rewrittenHtmlPaths` so the sibling
27
+ * `MigratePFluidAddImports` sub-recipe (which runs concurrently in the editor phase) can
28
+ * see the full set when its TS visitor fires.
29
+ */
30
+ class MigratePFluidHtml extends ScanningRecipe<{}> {
31
+ readonly name = "org.openrewrite.primeng.MigratePFluidToWrapper.html";
32
+ readonly displayName = "Rewrite `.p-fluid` divs to `<p-fluid>` wrappers";
33
+ readonly description = "Rewrites `<div class=\"…p-fluid…\">…</div>` to `<p-fluid class=\"…\">…</p-fluid>` " +
34
+ "in HTML templates (the `p-fluid` class is removed from the resulting class list). Tracks " +
35
+ "which files were modified so a follow-up sub-recipe can add the `FluidModule` import.";
36
+
37
+ initialValue(_ctx: ExecutionContext): {} {
38
+ return {};
39
+ }
40
+
41
+ async scanner(_acc: {}): Promise<TreeVisitor<any, ExecutionContext>> {
42
+ // Populate the shared rewritten-paths set during the scanner phase so the sibling
43
+ // import-adder recipe sees it before its editor runs.
44
+ return new class extends PlainTextVisitor<ExecutionContext> {
45
+ override async visitText(text: PlainText, p: ExecutionContext): Promise<PlainText | undefined> {
46
+ const t = await super.visitText(text, p) as PlainText;
47
+ if (!t) return t;
48
+ if (!t.sourcePath.endsWith('.html')) return t;
49
+ if (!/\bp-fluid\b/.test(t.text)) return t;
50
+ // Same rewrite logic, but only record the path — don't mutate the tree here.
51
+ if (rewriteHtml(t.text) !== t.text) {
52
+ rewrittenHtmlPaths.add(t.sourcePath);
53
+ }
54
+ return t;
55
+ }
56
+ }();
57
+ }
58
+
59
+ async editorWithData(_acc: {}): Promise<TreeVisitor<any, ExecutionContext>> {
60
+ return new class extends PlainTextVisitor<ExecutionContext> {
61
+ override async visitText(text: PlainText, p: ExecutionContext): Promise<PlainText | undefined> {
62
+ const t = await super.visitText(text, p) as PlainText;
63
+ if (!t) return t;
64
+ if (!rewrittenHtmlPaths.has(t.sourcePath)) return t;
65
+ const updated = rewriteHtml(t.text);
66
+ if (updated === t.text) return t;
67
+ return {...t, text: updated, snippets: []};
68
+ }
69
+ }();
70
+ }
71
+ }
72
+
73
+ /**
74
+ * Adds a `FluidModule` import line and prepends `FluidModule` to the `imports: [...]` array
75
+ * of every `*.component.ts` whose `templateUrl` points at an HTML file rewritten by
76
+ * `MigratePFluidHtml`, plus the root NgModule for non-standalone apps. Without that, Angular
77
+ * reports `'p-fluid' is not a known element`.
78
+ *
79
+ * Why prepend rather than append: a multi-line `imports: [..., DataViewModule,]` with trailing
80
+ * comma has trailing whitespace (`,\n `) before `]` whose AST location is fiddly to mutate
81
+ * cleanly. Prepending sidesteps that — we slot the new entry in front of the existing first
82
+ * element, reusing that element's prefix for indentation, and the framework's natural
83
+ * inter-element comma handles the separator. Module-import order is functionally irrelevant
84
+ * to Angular.
85
+ */
86
+ class MigratePFluidAddImports extends Recipe {
87
+ readonly name = "org.openrewrite.primeng.MigratePFluidToWrapper.add-imports";
88
+ readonly displayName = "Add `FluidModule` import to components using `<p-fluid>`";
89
+ readonly description = "For each `*.component.ts` whose `templateUrl` references an HTML file " +
90
+ "rewritten with `<p-fluid>` wrappers (and the root NgModule for non-standalone apps), " +
91
+ "adds `import { FluidModule } from 'primeng/fluid';` and prepends `FluidModule` to the " +
92
+ "component's or module's `imports: [...]` array.";
93
+
94
+ async editor(): Promise<TreeVisitor<any, ExecutionContext>> {
95
+ return new class extends JavaScriptVisitor<ExecutionContext> {
96
+ override async visitAnnotation(annotation: J.Annotation, p: ExecutionContext): Promise<J | undefined> {
97
+ let a = await super.visitAnnotation(annotation, p) as J.Annotation;
98
+ if (!a) return a;
99
+
100
+ const annotType = a.annotationType;
101
+ if (!isIdentifier(annotType)) return a;
102
+ const annotName = (annotType as J.Identifier).simpleName;
103
+ if (annotName !== 'Component' && annotName !== 'NgModule') return a;
104
+ if (!a.arguments?.elements?.length) return a;
105
+ const firstArg = a.arguments.elements[0].element;
106
+ if (firstArg.kind !== J.Kind.NewClass) return a;
107
+
108
+ const newClass = firstArg as J.NewClass;
109
+ if (!newClass.body) return a;
110
+
111
+ // Decide whether this site needs FluidModule:
112
+ // - @Component: only if its templateUrl points at a rewritten HTML.
113
+ // - @NgModule: only the root NgModule (`bootstrap: [...]` present), and only if
114
+ // any HTML in the project was rewritten — catch-all for non-standalone apps.
115
+ if (annotName === 'Component') {
116
+ const sourceFile = this.cursor.firstEnclosing((v: any): v is { sourcePath: string } =>
117
+ v && typeof v.sourcePath === 'string');
118
+ if (!templateUrlReferencesRewritten(newClass, sourceFile?.sourcePath)) return a;
119
+ } else {
120
+ if (rewrittenHtmlPaths.size === 0) return a;
121
+ const hasBootstrap = newClass.body.statements.some(s => {
122
+ const stmt = s.element;
123
+ if (stmt.kind !== JS.Kind.PropertyAssignment) return false;
124
+ const nameExpr = (stmt as JS.PropertyAssignment).name.element;
125
+ return isIdentifier(nameExpr) && (nameExpr as J.Identifier).simpleName === 'bootstrap';
126
+ });
127
+ if (!hasBootstrap) return a;
128
+ }
129
+
130
+ // Find the `imports: [...]` member.
131
+ let importsIndex = -1;
132
+ for (let i = 0; i < newClass.body.statements.length; i++) {
133
+ const stmt = newClass.body.statements[i].element;
134
+ if (stmt.kind !== JS.Kind.PropertyAssignment) continue;
135
+ const prop = stmt as JS.PropertyAssignment;
136
+ const nameExpr = prop.name.element;
137
+ if (!isIdentifier(nameExpr)) continue;
138
+ if ((nameExpr as J.Identifier).simpleName === 'imports') {
139
+ importsIndex = i;
140
+ break;
141
+ }
142
+ }
143
+
144
+ maybeAddImport(this, {
145
+ module: 'primeng/fluid',
146
+ member: 'FluidModule',
147
+ onlyIfReferenced: false,
148
+ });
149
+
150
+ if (importsIndex === -1) return a;
151
+
152
+ const importsStmt = newClass.body.statements[importsIndex].element as any;
153
+ const arr = importsStmt.initializer;
154
+ const container = arr?.initializer;
155
+ const existingElements = container?.elements ?? [];
156
+
157
+ // Idempotence: skip if FluidModule is already in the array.
158
+ for (const el of existingElements) {
159
+ const expr = el.element;
160
+ if (isIdentifier(expr) && (expr as J.Identifier).simpleName === 'FluidModule') {
161
+ return a;
162
+ }
163
+ }
164
+
165
+ // Prepend, not append. The new entry mirrors the prefix of the existing first
166
+ // element (so the indent matches in multi-line arrays). The existing first
167
+ // element's prefix needs no change — it becomes the second element, and the
168
+ // framework injects a comma + the original prefix as the inter-element separator.
169
+ const fluidIdentifier: J.Identifier = {
170
+ kind: J.Kind.Identifier,
171
+ id: a.id,
172
+ prefix: existingElements.length > 0 ? existingElements[0].element.prefix : {kind: 'org.openrewrite.java.tree.Space', whitespace: '', comments: []} as any,
173
+ markers: {kind: 'org.openrewrite.marker.Markers', id: a.id, markers: []},
174
+ annotations: [],
175
+ simpleName: 'FluidModule',
176
+ };
177
+ const fluidRP = {
178
+ kind: J.Kind.RightPadded,
179
+ element: fluidIdentifier,
180
+ after: {kind: 'org.openrewrite.java.tree.Space', whitespace: '', comments: []},
181
+ markers: {kind: 'org.openrewrite.marker.Markers', id: a.id, markers: []},
182
+ };
183
+
184
+ const updatedImportsStmt = {
185
+ ...importsStmt,
186
+ initializer: {
187
+ ...arr,
188
+ initializer: {
189
+ ...container,
190
+ elements: [fluidRP, ...existingElements],
191
+ },
192
+ },
193
+ };
194
+
195
+ const newStatements = [...newClass.body.statements];
196
+ newStatements[importsIndex] = {
197
+ ...newStatements[importsIndex],
198
+ element: updatedImportsStmt,
199
+ };
200
+
201
+ const newNewClass = {...newClass, body: {...newClass.body, statements: newStatements}};
202
+ return {
203
+ ...a,
204
+ arguments: {
205
+ ...a.arguments!,
206
+ elements: [{
207
+ ...a.arguments!.elements[0],
208
+ element: newNewClass,
209
+ }],
210
+ },
211
+ } as J.Annotation;
212
+ }
213
+ };
214
+ }
215
+ }
216
+
217
+ /**
218
+ * Migrates PrimeNG v17's `.p-fluid` CSS class — removed in v18 — to v18's `<p-fluid>`
219
+ * wrapper component.
220
+ *
221
+ * In v17, `.p-fluid` made child form controls `display: block; width: 100%`, which most
222
+ * apps used to stack labels above inputs. In v18 the class is a no-op, so pages silently
223
+ * lose that layout. The replacement is the `<p-fluid>` wrapper from `primeng/fluid`,
224
+ * which applies the same effect via design tokens.
225
+ *
226
+ * Two-phase recipe:
227
+ * 1. `MigratePFluidHtml` — rewrites `<div class="…p-fluid…">…</div>` to
228
+ * `<p-fluid class="…">…</p-fluid>` and tracks affected file paths.
229
+ * 2. `MigratePFluidAddImports` — adds `FluidModule` to component files whose `templateUrl`
230
+ * points at one of the rewritten HTML files.
231
+ *
232
+ * Limitations: rewrites only `<div>` containers (other elements like `<section>` are left
233
+ * for `MarkDeprecatedPrimengCssClasses` to flag). Custom CSS rules selecting `.p-fluid`
234
+ * stop matching after the rewrite — same trade-off PrimeNG itself made by removing the class.
235
+ */
236
+ export class MigratePFluidToWrapper extends Recipe {
237
+ readonly name = "org.openrewrite.primeng.MigratePFluidToWrapper";
238
+ readonly displayName = "Migrate `.p-fluid` to `<p-fluid>` wrapper";
239
+ readonly description = "Rewrites `<div class=\"…p-fluid…\">…</div>` to `<p-fluid class=\"…\">…</p-fluid>` " +
240
+ "and adds a `FluidModule` import from `primeng/fluid` to the corresponding component file. " +
241
+ "PrimeNG 18 removed the `.p-fluid` CSS class; the `<p-fluid>` wrapper component is its " +
242
+ "replacement.";
243
+
244
+ async recipeList(): Promise<Recipe[]> {
245
+ // Reset shared state for each top-level invocation; each call to `recipeList()` produces
246
+ // a fresh chain instance, but the module-scoped Set persists across them otherwise.
247
+ rewrittenHtmlPaths.clear();
248
+ return [
249
+ new MigratePFluidHtml(),
250
+ new MigratePFluidAddImports(),
251
+ ];
252
+ }
253
+ }
254
+
255
+ /**
256
+ * Rewrite every `<div class="…p-fluid…">CHILDREN</div>` in the given source text. Two cases:
257
+ *
258
+ * 1. `p-fluid` is the only class on the div → replace the div entirely with `<p-fluid>`:
259
+ * `<div class="p-fluid">CHILDREN</div>` → `<p-fluid>CHILDREN</p-fluid>`
260
+ *
261
+ * 2. The div has other classes → keep the original `<div>` (preserving its natural
262
+ * content-width and any custom CSS targeting its non-`p-fluid` classes) and wrap
263
+ * CHILDREN in a `<p-fluid>`. The `p-fluid` class is stripped from the div:
264
+ * `<div class="X p-fluid">CHILDREN</div>` → `<div class="X"><p-fluid>CHILDREN</p-fluid></div>`
265
+ *
266
+ * Case 2 prevents the v18 `<p-fluid>` element's `display: block; width: 100%;` from
267
+ * forcing the wrapper to fill its flex parent (a regression vs. v17's `<div class="…p-fluid">`,
268
+ * whose own width was content-driven — only child inputs got 100% from `.p-fluid`).
269
+ */
270
+ function rewriteHtml(src: string): string {
271
+ if (!/\bp-fluid\b/.test(src)) return src;
272
+
273
+ const divOpenRe = /<div\b([^>]*)>/g;
274
+ type Rewrite = {
275
+ openStart: number;
276
+ openEnd: number;
277
+ closeStart: number;
278
+ closeEnd: number;
279
+ replaceDiv: boolean; // true = replace <div> with <p-fluid>; false = wrap children
280
+ newOpenTag: string; // the rewritten opening tag
281
+ };
282
+ const rewrites: Rewrite[] = [];
283
+
284
+ let m: RegExpExecArray | null;
285
+ while ((m = divOpenRe.exec(src)) !== null) {
286
+ const attrs = m[1];
287
+ // Skip self-closing `<div ... />` — no body to wrap.
288
+ if (attrs.trimEnd().endsWith('/')) continue;
289
+
290
+ const classMatch = attrs.match(/(?:^|\s)class\s*=\s*("([^"]*)"|'([^']*)')/);
291
+ if (!classMatch) continue;
292
+ const classValue = classMatch[2] ?? classMatch[3];
293
+ if (!/\bp-fluid\b/.test(classValue)) continue;
294
+
295
+ const openEnd = divOpenRe.lastIndex;
296
+ const closePos = findMatchingDivClose(src, openEnd);
297
+ if (closePos === -1) continue; // Unbalanced HTML; skip.
298
+
299
+ const otherClasses = classValue
300
+ .split(/\s+/)
301
+ .filter(c => c && c !== 'p-fluid')
302
+ .join(' ')
303
+ .trim();
304
+
305
+ const openTag = src.slice(m.index, openEnd);
306
+
307
+ let replaceDiv: boolean;
308
+ let newOpenTag: string;
309
+ if (otherClasses === '' && /<div\s+class\s*=\s*("[^"]*"|'[^']*')\s*>/.test(openTag)) {
310
+ // `p-fluid` was the sole class AND the div had no other attributes → drop the
311
+ // div entirely and replace with `<p-fluid>`.
312
+ replaceDiv = true;
313
+ newOpenTag = '<p-fluid>';
314
+ } else {
315
+ // Keep the original div; just strip `p-fluid` from its class list (or remove the
316
+ // class attribute entirely if `p-fluid` was the only entry there). The `<p-fluid>`
317
+ // wrapper goes around CHILDREN inside.
318
+ replaceDiv = false;
319
+ const newClassAttr = otherClasses ? ` class="${otherClasses}"` : '';
320
+ newOpenTag = openTag.replace(/\s+class\s*=\s*("[^"]*"|'[^']*')/, newClassAttr);
321
+ }
322
+
323
+ rewrites.push({
324
+ openStart: m.index,
325
+ openEnd,
326
+ closeStart: closePos,
327
+ closeEnd: closePos + '</div>'.length,
328
+ replaceDiv,
329
+ newOpenTag,
330
+ });
331
+ }
332
+
333
+ if (rewrites.length === 0) return src;
334
+
335
+ // Apply rewrites from the end of the string forward so earlier offsets stay valid.
336
+ let updated = src;
337
+ for (let i = rewrites.length - 1; i >= 0; i--) {
338
+ const r = rewrites[i];
339
+ if (r.replaceDiv) {
340
+ // Replace `<div class="p-fluid">…</div>` with `<p-fluid>…</p-fluid>`.
341
+ updated =
342
+ updated.slice(0, r.openStart) +
343
+ '<p-fluid>' +
344
+ updated.slice(r.openEnd, r.closeStart) +
345
+ '</p-fluid>' +
346
+ updated.slice(r.closeEnd);
347
+ } else {
348
+ // Keep the div, but rewrite its open tag (drop the p-fluid class) and wrap
349
+ // children in <p-fluid>. We need the indentation of the FIRST child line and
350
+ // of the closing `</div>` so `<p-fluid>` tags align naturally; preserve
351
+ // whatever the existing children indent looked like by using the existing
352
+ // child-block whitespace verbatim.
353
+ const childrenBlock = updated.slice(r.openEnd, r.closeStart);
354
+ // Insert <p-fluid> on its own line if the original had multi-line children;
355
+ // otherwise put it inline. We detect multi-line by the presence of `\n` in
356
+ // the children block.
357
+ let wrappedChildren: string;
358
+ if (childrenBlock.includes('\n')) {
359
+ // Match the leading whitespace of the first non-empty child line so the
360
+ // <p-fluid> tags sit one indent level outside the children.
361
+ const childIndentMatch = childrenBlock.match(/\n([ \t]*)\S/);
362
+ const childIndent = childIndentMatch?.[1] ?? '';
363
+ // Outer indent (where </div> sits) is one level less; approximate by
364
+ // dropping 2 chars of the child indent (good enough for 2-space-indent
365
+ // codebases; falls back to '' for 1-char or no indent).
366
+ const outerIndent = childIndent.length >= 2 ? childIndent.slice(0, -2) : '';
367
+ wrappedChildren =
368
+ `\n${childIndent}<p-fluid>` +
369
+ childrenBlock.replace(/\n/g, `\n `) +
370
+ `</p-fluid>\n${outerIndent}`;
371
+ } else {
372
+ wrappedChildren = `<p-fluid>${childrenBlock}</p-fluid>`;
373
+ }
374
+ updated =
375
+ updated.slice(0, r.openStart) +
376
+ r.newOpenTag +
377
+ wrappedChildren +
378
+ '</div>' +
379
+ updated.slice(r.closeEnd);
380
+ }
381
+ }
382
+ return updated;
383
+ }
384
+
385
+ /**
386
+ * From the position immediately after a `<div ...>` opening tag, return the index where
387
+ * its matching `</div>` close tag starts. Returns -1 if the file is malformed.
388
+ */
389
+ function findMatchingDivClose(src: string, startPos: number): number {
390
+ let depth = 1;
391
+ let i = startPos;
392
+ while (i < src.length) {
393
+ const openIdx = src.indexOf('<div', i);
394
+ const closeIdx = src.indexOf('</div>', i);
395
+ if (closeIdx === -1) return -1;
396
+ if (openIdx !== -1 && openIdx < closeIdx) {
397
+ const tagEnd = src.indexOf('>', openIdx);
398
+ if (tagEnd === -1) return -1;
399
+ const tag = src.slice(openIdx, tagEnd + 1);
400
+ if (!tag.trimEnd().endsWith('/>')) depth++;
401
+ i = tagEnd + 1;
402
+ } else {
403
+ depth--;
404
+ if (depth === 0) return closeIdx;
405
+ i = closeIdx + '</div>'.length;
406
+ }
407
+ }
408
+ return -1;
409
+ }
410
+
411
+ /**
412
+ * Given a `@Component({...})` argument NewClass and the source path of the TS file, check
413
+ * whether its `templateUrl` resolves to a path in the rewritten-HTML set.
414
+ */
415
+ function templateUrlReferencesRewritten(newClass: J.NewClass, tsSourcePath: string | undefined): boolean {
416
+ if (!tsSourcePath) return false;
417
+ if (!newClass.body) return false;
418
+
419
+ for (const stmt of newClass.body.statements) {
420
+ const s = stmt.element;
421
+ if (s.kind !== JS.Kind.PropertyAssignment) continue;
422
+ const prop = s as JS.PropertyAssignment;
423
+ const nameExpr = prop.name.element;
424
+ if (!isIdentifier(nameExpr)) continue;
425
+ if ((nameExpr as J.Identifier).simpleName !== 'templateUrl') continue;
426
+ const valueExpr: any = (prop as any).initializer?.element ?? (prop as any).initializer;
427
+ if (valueExpr?.kind !== J.Kind.Literal) continue;
428
+ const templatePath = String((valueExpr as J.Literal).value ?? '');
429
+ const tsDir = path.dirname(tsSourcePath);
430
+ const resolved = path.normalize(path.join(tsDir, templatePath));
431
+ for (const htmlPath of rewrittenHtmlPaths) {
432
+ if (htmlPath === resolved || htmlPath.endsWith(resolved) || resolved.endsWith(htmlPath)) {
433
+ return true;
434
+ }
435
+ }
436
+ return false;
437
+ }
438
+ return false;
439
+ }
@@ -0,0 +1,112 @@
1
+ /*
2
+ * Copyright 2026 the original author or authors.
3
+ *
4
+ * Moderne Proprietary. Only for use by Moderne customers under the terms of a commercial contract.
5
+ */
6
+
7
+ import {ExecutionContext, foundSearchResult, produceAsync, Recipe, TreeVisitor} from "@openrewrite/rewrite";
8
+ import {ChangeImport, JavaScriptVisitor, JS} from "@openrewrite/rewrite/javascript";
9
+ import {J, isIdentifier} from "@openrewrite/rewrite/java";
10
+
11
+ const CONFIG_NAMES = new Set(['PrimeNGConfig', 'PrimeNG']);
12
+
13
+ class RenamePrimeNGConfigIdentifiers extends Recipe {
14
+ readonly name = "org.openrewrite.primeng.MigratePrimeNgConfigToPrimeNG.identifiers";
15
+ readonly displayName = "Rename `PrimeNGConfig` identifiers to `PrimeNG`";
16
+ readonly description = "Renames `PrimeNGConfig` identifier usages in files importing it from `primeng/api`.";
17
+
18
+ async editor(): Promise<TreeVisitor<any, ExecutionContext>> {
19
+ return new class extends JavaScriptVisitor<ExecutionContext> {
20
+ private hasImport = false;
21
+
22
+ protected async visitImportDeclaration(jsImport: JS.Import, p: ExecutionContext): Promise<J | undefined> {
23
+ const imp = await super.visitImportDeclaration(jsImport, p) as JS.Import;
24
+ if (!imp?.moduleSpecifier) return imp;
25
+ const moduleSpec = imp.moduleSpecifier.element;
26
+ if (moduleSpec.kind !== J.Kind.Literal) return imp;
27
+ if ((moduleSpec as J.Literal).value !== "primeng/api") return imp;
28
+
29
+ const namedBindings = imp.importClause?.namedBindings;
30
+ if (namedBindings?.kind === JS.Kind.NamedImports) {
31
+ for (const specifier of (namedBindings as JS.NamedImports).elements.elements) {
32
+ const spec = specifier.element as JS.ImportSpecifier;
33
+ if (isIdentifier(spec.specifier) && (spec.specifier as J.Identifier).simpleName === 'PrimeNGConfig') {
34
+ this.hasImport = true;
35
+ }
36
+ }
37
+ }
38
+ return imp;
39
+ }
40
+
41
+ override async visitIdentifier(identifier: J.Identifier, p: ExecutionContext): Promise<J | undefined> {
42
+ const id = await super.visitIdentifier(identifier, p) as J.Identifier;
43
+ if (!id) return id;
44
+ if (this.hasImport && id.simpleName === 'PrimeNGConfig') {
45
+ return produceAsync(id, async draft => { draft.simpleName = 'PrimeNG'; });
46
+ }
47
+ return id;
48
+ }
49
+ };
50
+ }
51
+ }
52
+
53
+ class FindPrimeNGConfigInjection extends Recipe {
54
+ readonly name = "org.openrewrite.primeng.MigratePrimeNgConfigToPrimeNG.find-injection";
55
+ readonly displayName = "Find `PrimeNGConfig` / `PrimeNG` service injection";
56
+ readonly description = "Flags constructor injection or `inject()` calls for `PrimeNGConfig` or `PrimeNG` service, " +
57
+ "suggesting migration to `providePrimeNG()` in application providers.";
58
+
59
+ async editor(): Promise<TreeVisitor<any, ExecutionContext>> {
60
+ return new class extends JavaScriptVisitor<ExecutionContext> {
61
+ protected async visitMethodInvocation(method: J.MethodInvocation, p: ExecutionContext): Promise<J | undefined> {
62
+ const m = await super.visitMethodInvocation(method, p) as J.MethodInvocation;
63
+ if (!m) return m;
64
+
65
+ if (isIdentifier(m.name) && m.name.simpleName === 'inject') {
66
+ for (const arg of m.arguments?.elements ?? []) {
67
+ if (isIdentifier(arg.element) && CONFIG_NAMES.has((arg.element as J.Identifier).simpleName)) {
68
+ return foundSearchResult(m,
69
+ `\`${(arg.element as J.Identifier).simpleName}\` injection detected. ` +
70
+ "Migrate to `providePrimeNG({ ... })` in your application providers (e.g. `app.config.ts`) instead of injecting the service directly.");
71
+ }
72
+ }
73
+ }
74
+
75
+ return m;
76
+ }
77
+
78
+ protected async visitVariableDeclarations(variable: J.VariableDeclarations, p: ExecutionContext): Promise<J | undefined> {
79
+ const v = await super.visitVariableDeclarations(variable, p) as J.VariableDeclarations;
80
+ if (!v) return v;
81
+
82
+ if (v.typeExpression && isIdentifier(v.typeExpression) && CONFIG_NAMES.has(v.typeExpression.simpleName)) {
83
+ return foundSearchResult(v,
84
+ `\`${v.typeExpression.simpleName}\` type annotation detected. ` +
85
+ "Migrate to `providePrimeNG({ ... })` in your application providers (e.g. `app.config.ts`) instead of injecting the service directly.");
86
+ }
87
+
88
+ return v;
89
+ }
90
+ };
91
+ }
92
+ }
93
+
94
+ export class MigratePrimeNgConfigToPrimeNG extends Recipe {
95
+ readonly name = "org.openrewrite.primeng.MigratePrimeNgConfigToPrimeNG";
96
+ readonly displayName = "Migrate `PrimeNGConfig` to `PrimeNG`";
97
+ readonly description = "Renames the `PrimeNGConfig` import from `primeng/api` to `PrimeNG` from `primeng/config`, " +
98
+ "renames all identifier usages, and flags injection sites that should be migrated to `providePrimeNG()` in application providers.";
99
+
100
+ async recipeList(): Promise<Recipe[]> {
101
+ return [
102
+ new RenamePrimeNGConfigIdentifiers(),
103
+ new ChangeImport({
104
+ oldModule: "primeng/api",
105
+ oldMember: "PrimeNGConfig",
106
+ newModule: "primeng/config",
107
+ newMember: "PrimeNG",
108
+ }),
109
+ new FindPrimeNGConfigInjection(),
110
+ ];
111
+ }
112
+ }
@@ -0,0 +1,83 @@
1
+ /*
2
+ * Copyright 2026 the original author or authors.
3
+ *
4
+ * Moderne Proprietary. Only for use by Moderne customers under the terms of a commercial contract.
5
+ */
6
+
7
+ import {ExecutionContext, Recipe, TreeVisitor} from "@openrewrite/rewrite";
8
+ import {JavaScriptVisitor, JS, Template} from "@openrewrite/rewrite/javascript";
9
+ import {J, isIdentifier} from "@openrewrite/rewrite/java";
10
+
11
+ // Fields on the v18 `PrimeNG` config service that are now `WritableSignal<T>`.
12
+ // In v17 they were plain fields, so direct assignment worked.
13
+ // In v18 the assignment must go through `.set(value)`.
14
+ const SIGNAL_FIELDS = new Set<string>([
15
+ 'ripple',
16
+ 'inputStyle',
17
+ 'inputVariant',
18
+ 'csp',
19
+ ]);
20
+
21
+ /**
22
+ * Converts assignments like `service.ripple = true` to `service.ripple.set(true)`
23
+ * in files that import `PrimeNG` from `primeng/config` (i.e. the v18 config service).
24
+ */
25
+ export class MigratePrimeNGSignalAssignments extends Recipe {
26
+ readonly name = "org.openrewrite.primeng.MigratePrimeNGSignalAssignments";
27
+ readonly displayName = "Migrate `PrimeNG` config field assignments to `.set()`";
28
+ readonly description = "In PrimeNG 18, fields on the `PrimeNG` config service like `ripple`, " +
29
+ "`inputStyle`, `inputVariant`, and `csp` are `WritableSignal<T>` rather than plain fields. " +
30
+ "Direct assignment (`service.ripple = true`) no longer compiles. This recipe rewrites such " +
31
+ "assignments to use the signal's `set()` method (`service.ripple.set(true)`) when the file " +
32
+ "imports `PrimeNG` from `primeng/config`.";
33
+
34
+ async editor(): Promise<TreeVisitor<any, ExecutionContext>> {
35
+ return new class extends JavaScriptVisitor<ExecutionContext> {
36
+ private hasPrimeNGImport = false;
37
+
38
+ protected async visitImportDeclaration(jsImport: JS.Import, p: ExecutionContext): Promise<J | undefined> {
39
+ const imp = await super.visitImportDeclaration(jsImport, p) as JS.Import;
40
+ if (!imp?.moduleSpecifier) return imp;
41
+ const moduleSpec = imp.moduleSpecifier.element;
42
+ if (moduleSpec.kind !== J.Kind.Literal) return imp;
43
+ if ((moduleSpec as J.Literal).value !== "primeng/config") return imp;
44
+
45
+ const namedBindings = imp.importClause?.namedBindings;
46
+ if (namedBindings?.kind === JS.Kind.NamedImports) {
47
+ for (const specifier of (namedBindings as JS.NamedImports).elements.elements) {
48
+ const spec = specifier.element as JS.ImportSpecifier;
49
+ if (isIdentifier(spec.specifier) && (spec.specifier as J.Identifier).simpleName === 'PrimeNG') {
50
+ this.hasPrimeNGImport = true;
51
+ }
52
+ }
53
+ }
54
+ return imp;
55
+ }
56
+
57
+ protected async visitAssignment(assignment: J.Assignment, p: ExecutionContext): Promise<J | undefined> {
58
+ const a = await super.visitAssignment(assignment, p) as J.Assignment;
59
+ if (!a) return a;
60
+ if (!this.hasPrimeNGImport) return a;
61
+
62
+ const variable = a.variable;
63
+ if (variable.kind !== J.Kind.FieldAccess) return a;
64
+
65
+ const fa = variable as J.FieldAccess;
66
+ const fieldName = (fa.name as any)?.element?.simpleName ?? (fa.name as any)?.simpleName;
67
+ if (!fieldName || !SIGNAL_FIELDS.has(fieldName)) return a;
68
+
69
+ const valueNode = (a.assignment as any).element ?? a.assignment;
70
+
71
+ const replacement = await Template.builder()
72
+ .param(fa)
73
+ .code('.set(')
74
+ .param(valueNode)
75
+ .code(')')
76
+ .build()
77
+ .apply(a, this.cursor);
78
+
79
+ return {...(replacement as any), prefix: a.prefix} as J;
80
+ }
81
+ };
82
+ }
83
+ }