@c15t/cli 2.0.0-rc.5 → 2.0.0-rc.6

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,1797 @@
1
+ import promises from "node:fs/promises";
2
+ import node_path from "node:path";
3
+ import picocolors from "picocolors";
4
+ import { Node, Project, SyntaxKind } from "ts-morph";
5
+ import { logger_formatLogMessage, STORAGE_MODES, LAYOUT_PATTERNS, PAGES_APP_PATTERNS as constants_PAGES_APP_PATTERNS, constants_REGEX } from "./145.mjs";
6
+ function generateClientConfigContent(mode, backendURL, useEnvFile, enableDevTools = false) {
7
+ switch(mode){
8
+ case STORAGE_MODES.HOSTED:
9
+ case STORAGE_MODES.C15T:
10
+ return generateHostedConfig(backendURL, useEnvFile, enableDevTools);
11
+ case STORAGE_MODES.OFFLINE:
12
+ return generateOfflineConfig(enableDevTools);
13
+ case STORAGE_MODES.SELF_HOSTED:
14
+ return generateSelfHostedConfig(backendURL, useEnvFile, enableDevTools);
15
+ case STORAGE_MODES.CUSTOM:
16
+ return generateCustomConfig(backendURL, useEnvFile, enableDevTools);
17
+ default:
18
+ return generateOfflineConfig(enableDevTools);
19
+ }
20
+ }
21
+ function generateHostedConfig(backendURL, useEnvFile, enableDevTools = false) {
22
+ const url = useEnvFile ? 'process.env.NEXT_PUBLIC_C15T_URL' : `'${backendURL || 'https://your-instance.c15t.dev'}'`;
23
+ const devToolsImport = enableDevTools ? "import { createDevTools } from '@c15t/dev-tools';\n" : '';
24
+ const devToolsCall = enableDevTools ? 'createDevTools();\n' : '';
25
+ return `import { getOrCreateConsentRuntime } from 'c15t';
26
+ ${devToolsImport}
27
+ const runtime = getOrCreateConsentRuntime(
28
+ {
29
+ mode: 'hosted',
30
+ backendURL: ${url},
31
+ consentCategories: ['necessary', 'measurement', 'marketing'],
32
+ },
33
+ );
34
+
35
+ export const store = runtime.consentStore;
36
+ ${devToolsCall}
37
+ /**
38
+ * Usage Examples
39
+ **/
40
+
41
+ // View all consents
42
+ // store.getState().consents;
43
+
44
+ // Update a single consent type: (does not save automically, allowing you to batch updates together before saving)
45
+ // store.getState().setSelectedConsent('measurement', true);
46
+
47
+ // Update a single consent type and automically saves it
48
+ // store.getState().setConsent('marketing', true);
49
+
50
+ // When a user rejects all consents:
51
+ // store.getState().saveConsents("necessary")
52
+ `;
53
+ }
54
+ function generateOfflineConfig(enableDevTools = false) {
55
+ const devToolsImport = enableDevTools ? "import { createDevTools } from '@c15t/dev-tools';\n" : '';
56
+ const devToolsCall = enableDevTools ? 'createDevTools();\n' : '';
57
+ return `import { getOrCreateConsentRuntime } from 'c15t';
58
+ ${devToolsImport}
59
+ const runtime = getOrCreateConsentRuntime(
60
+ {
61
+ mode: 'offline',
62
+ consentCategories: ['necessary', 'measurement', 'marketing'],
63
+ },
64
+ );
65
+
66
+ export const store = runtime.consentStore;
67
+ ${devToolsCall}
68
+ /**
69
+ * Usage Examples
70
+ **/
71
+
72
+ // View all consents
73
+ // store.getState().consents;
74
+
75
+ // Update a single consent type: (does not save automically, allowing you to batch updates together before saving)
76
+ // store.getState().setSelectedConsent('measurement', true);
77
+
78
+ // Update a single consent type and automically saves it
79
+ // store.getState().setConsent('marketing', true);
80
+
81
+ // When a user rejects all consents:
82
+ // store.getState().saveConsents("necessary")
83
+ `;
84
+ }
85
+ function generateSelfHostedConfig(backendURL, useEnvFile, enableDevTools = false) {
86
+ const url = useEnvFile ? 'process.env.NEXT_PUBLIC_C15T_URL' : `'${backendURL || 'http://localhost:3001'}'`;
87
+ const devToolsImport = enableDevTools ? "import { createDevTools } from '@c15t/dev-tools';\n" : '';
88
+ const devToolsCall = enableDevTools ? 'createDevTools();\n' : '';
89
+ return `import { getOrCreateConsentRuntime } from 'c15t';
90
+ ${devToolsImport}
91
+ const runtime = getOrCreateConsentRuntime(
92
+ {
93
+ mode: 'hosted',
94
+ backendURL: ${url},
95
+ consentCategories: ['necessary', 'measurement', 'marketing'],
96
+ },
97
+ );
98
+
99
+ export const store = runtime.consentStore;
100
+ ${devToolsCall}
101
+ /**
102
+ * Usage Examples
103
+ **/
104
+
105
+ // View all consents
106
+ // store.getState().consents;
107
+
108
+ // Update a single consent type: (does not save automically, allowing you to batch updates together before saving)
109
+ // store.getState().setSelectedConsent('measurement', true);
110
+
111
+ // Update a single consent type and automically saves it
112
+ // store.getState().setConsent('marketing', true);
113
+
114
+ // When a user rejects all consents:
115
+ // store.getState().saveConsents("necessary")
116
+ `;
117
+ }
118
+ function generateCustomConfig(backendURL, useEnvFile, enableDevTools = false) {
119
+ const url = useEnvFile ? 'process.env.NEXT_PUBLIC_CONSENT_API_URL' : `'${backendURL || '/api/consent'}'`;
120
+ const devToolsImport = enableDevTools ? "import { createDevTools } from '@c15t/dev-tools';\n" : '';
121
+ const devToolsCall = enableDevTools ? 'createDevTools();\n' : '';
122
+ return `import { getOrCreateConsentRuntime, type EndpointHandlers } from 'c15t';
123
+ ${devToolsImport}
124
+ function createCustomHandlers(): EndpointHandlers {
125
+ return {
126
+ async getConsent() {
127
+ const response = await fetch(${url});
128
+ return response.json();
129
+ },
130
+ async setConsent(consent) {
131
+ const response = await fetch(${url}, {
132
+ method: 'POST',
133
+ headers: { 'Content-Type': 'application/json' },
134
+ body: JSON.stringify(consent),
135
+ });
136
+ return response.json();
137
+ },
138
+ };
139
+ }
140
+
141
+ const runtime = getOrCreateConsentRuntime(
142
+ {
143
+ mode: 'custom',
144
+ endpointHandlers: createCustomHandlers(),
145
+ consentCategories: ['necessary', 'measurement', 'marketing'],
146
+ },
147
+ );
148
+
149
+ export const store = runtime.consentStore;
150
+ ${devToolsCall}
151
+ /**
152
+ * Usage Examples
153
+ **/
154
+
155
+ // View all consents
156
+ // store.getState().consents;
157
+
158
+ // Update a single consent type: (does not save automically, allowing you to batch updates together before saving)
159
+ // store.getState().setSelectedConsent('measurement', true);
160
+
161
+ // Update a single consent type and automically saves it
162
+ // store.getState().setConsent('marketing', true);
163
+
164
+ // When a user rejects all consents:
165
+ // store.getState().saveConsents("necessary")
166
+ `;
167
+ }
168
+ const CSS_PATTERNS = [
169
+ 'app/globals.css',
170
+ 'src/app/globals.css',
171
+ 'app/global.css',
172
+ 'src/app/global.css',
173
+ 'styles/globals.css',
174
+ 'src/styles/globals.css',
175
+ 'styles/global.css',
176
+ 'src/styles/global.css',
177
+ 'src/index.css',
178
+ 'src/App.css'
179
+ ];
180
+ async function updateTailwindCss(projectRoot, tailwindVersion) {
181
+ if (!tailwindVersion || !tailwindVersion.match(/^(?:\^|~)?3/)) return {
182
+ updated: false,
183
+ filePath: null
184
+ };
185
+ for (const pattern of CSS_PATTERNS){
186
+ const filePath = node_path.join(projectRoot, pattern);
187
+ try {
188
+ await promises.access(filePath);
189
+ const content = await promises.readFile(filePath, 'utf-8');
190
+ if (content.includes('@layer base, components, c15t;') || content.includes('@layer base, components;')) return {
191
+ updated: false,
192
+ filePath
193
+ };
194
+ if (content.includes('@tailwind base') || content.includes('@tailwind components') || content.includes('@tailwind utilities')) {
195
+ let newContent = content;
196
+ if (newContent.includes('@tailwind base')) {
197
+ newContent = newContent.replace('@tailwind base;', '@layer base {\n @tailwind base;\n}');
198
+ newContent = newContent.replace('@tailwind base\n', '@layer base {\n @tailwind base;\n}\n');
199
+ }
200
+ if (!newContent.includes('@layer base, components, c15t;')) newContent = `@layer base, components, c15t;\n\n${newContent}`;
201
+ await promises.writeFile(filePath, newContent, 'utf-8');
202
+ return {
203
+ updated: true,
204
+ filePath
205
+ };
206
+ }
207
+ } catch {}
208
+ }
209
+ return {
210
+ updated: false,
211
+ filePath: null
212
+ };
213
+ }
214
+ function getEnvVarName(pkg) {
215
+ return '@c15t/nextjs' === pkg ? 'NEXT_PUBLIC_C15T_URL' : 'PUBLIC_C15T_URL';
216
+ }
217
+ function generateEnvFileContent(backendURL, pkg) {
218
+ const envVarName = getEnvVarName(pkg);
219
+ return `\n${envVarName}=${backendURL}\n`;
220
+ }
221
+ function generateEnvExampleContent(pkg) {
222
+ const envVarName = getEnvVarName(pkg);
223
+ return `\n# c15t Configuration\n${envVarName}=https://your-instance.c15t.dev\n`;
224
+ }
225
+ async function findMatchingFiles(projectRoot, patterns, logger) {
226
+ const matches = [];
227
+ for (const pattern of patterns){
228
+ const parts = pattern.split('/');
229
+ const hasWildcard = parts.some((p)=>'*' === p);
230
+ if (hasWildcard) {
231
+ const baseParts = [];
232
+ for (const part of parts){
233
+ if ('*' === part) break;
234
+ baseParts.push(part);
235
+ }
236
+ const baseDir = node_path.join(projectRoot, ...baseParts);
237
+ try {
238
+ const entries = await promises.readdir(baseDir, {
239
+ withFileTypes: true
240
+ });
241
+ for (const entry of entries)if (entry.isDirectory()) {
242
+ const remainingParts = parts.slice(baseParts.length + 1);
243
+ const potentialPath = node_path.join(baseDir, entry.name, ...remainingParts);
244
+ const relativePath = node_path.relative(projectRoot, potentialPath);
245
+ const patternWithDir = pattern.replace('*', entry.name);
246
+ if (relativePath === patternWithDir.replace(/\//g, node_path.sep)) try {
247
+ await promises.access(potentialPath);
248
+ logger?.debug(`Found layout: ${relativePath}`);
249
+ matches.push(relativePath);
250
+ } catch {}
251
+ }
252
+ } catch {}
253
+ } else {
254
+ const filePath = node_path.join(projectRoot, pattern);
255
+ try {
256
+ await promises.access(filePath);
257
+ logger?.debug(`Found layout: ${pattern}`);
258
+ matches.push(pattern);
259
+ } catch {}
260
+ }
261
+ }
262
+ return matches;
263
+ }
264
+ function extractLocaleSegment(filepath) {
265
+ const match = filepath.match(constants_REGEX.DYNAMIC_SEGMENT);
266
+ return match ? match[0] : void 0;
267
+ }
268
+ function hasLocaleSegment(filepath) {
269
+ return constants_REGEX.DYNAMIC_SEGMENT.test(filepath);
270
+ }
271
+ function getAppDirectory(layoutPath) {
272
+ const parts = layoutPath.split(node_path.sep);
273
+ const appIndex = parts.indexOf('app');
274
+ if (-1 === appIndex) {
275
+ const pagesIndex = parts.indexOf('pages');
276
+ if (-1 !== pagesIndex) return parts.slice(0, pagesIndex + 1).join(node_path.sep);
277
+ return node_path.dirname(layoutPath);
278
+ }
279
+ if (hasLocaleSegment(layoutPath)) return parts.slice(0, appIndex + 2).join(node_path.sep);
280
+ return parts.slice(0, appIndex + 1).join(node_path.sep);
281
+ }
282
+ async function findLayoutFile(projectRoot, logger) {
283
+ logger?.debug(`Searching for layout file in ${projectRoot}`);
284
+ const appLayoutMatches = await findMatchingFiles(projectRoot, LAYOUT_PATTERNS, logger);
285
+ if (appLayoutMatches.length > 0) {
286
+ appLayoutMatches.sort((a, b)=>{
287
+ const aHasLocale = hasLocaleSegment(a);
288
+ const bHasLocale = hasLocaleSegment(b);
289
+ if (!aHasLocale && bHasLocale) return -1;
290
+ if (aHasLocale && !bHasLocale) return 1;
291
+ return a.length - b.length;
292
+ });
293
+ const layoutPath = appLayoutMatches[0];
294
+ const localeSegment = extractLocaleSegment(layoutPath);
295
+ logger?.debug(`Selected layout: ${layoutPath}`);
296
+ return {
297
+ path: layoutPath,
298
+ type: 'app',
299
+ hasLocaleSegment: !!localeSegment,
300
+ localeSegment,
301
+ appDirectory: getAppDirectory(layoutPath)
302
+ };
303
+ }
304
+ const pagesLayoutMatches = await findMatchingFiles(projectRoot, constants_PAGES_APP_PATTERNS, logger);
305
+ if (pagesLayoutMatches.length > 0) {
306
+ const layoutPath = pagesLayoutMatches[0];
307
+ logger?.debug(`Selected pages layout: ${layoutPath}`);
308
+ return {
309
+ path: layoutPath,
310
+ type: 'pages',
311
+ hasLocaleSegment: false,
312
+ appDirectory: getAppDirectory(layoutPath)
313
+ };
314
+ }
315
+ logger?.debug('No layout file found');
316
+ return null;
317
+ }
318
+ function toCamelCase(scriptName) {
319
+ return scriptName.replace(/-([a-z])/g, (_, letter)=>letter.toUpperCase());
320
+ }
321
+ function generateScriptsImport(selectedScripts) {
322
+ if (!selectedScripts.length) return '';
323
+ return selectedScripts.map((script)=>`import { ${toCamelCase(script)} } from '@c15t/scripts/${script}';`).join('\n');
324
+ }
325
+ function generateScriptsConfig(selectedScripts) {
326
+ if (!selectedScripts.length) return '';
327
+ const scriptConfigs = selectedScripts.map((script)=>{
328
+ const funcName = toCamelCase(script);
329
+ const idPlaceholder = getIdPlaceholder(script);
330
+ return `${funcName}({ id: '${idPlaceholder}' })`;
331
+ });
332
+ return `scripts: [
333
+ ${scriptConfigs.join(',\n\t\t\t\t\t')},
334
+ ],`;
335
+ }
336
+ function getIdPlaceholder(scriptName) {
337
+ switch(scriptName){
338
+ case 'google-tag-manager':
339
+ return 'GTM-XXXXXX';
340
+ case 'google-tag':
341
+ return 'G-XXXXXXXXXX';
342
+ case 'meta-pixel':
343
+ return 'XXXXXXXXXXXXXXXXXX';
344
+ case 'posthog':
345
+ return 'phc_XXXXXXXXXX';
346
+ case 'linkedin-insights':
347
+ return 'XXXXXXX';
348
+ case 'tiktok-pixel':
349
+ return 'XXXXXXXXXXXXXXXXX';
350
+ case 'x-pixel':
351
+ return 'XXXXX';
352
+ case 'microsoft-uet':
353
+ return 'XXXXXXXXXX';
354
+ case 'databuddy':
355
+ return 'YOUR_ID_HERE';
356
+ default:
357
+ return 'YOUR_ID_HERE';
358
+ }
359
+ }
360
+ function generateScriptsCommentPlaceholder() {
361
+ return `// Add your scripts here:
362
+ // import { googleTagManager } from '@c15t/scripts/google-tag-manager';
363
+ // scripts: [
364
+ // googleTagManager({ id: 'GTM-XXXXXX' }),
365
+ // ],`;
366
+ }
367
+ function generateConsentComponent({ importSource, optionsText, selectedScripts = [], initialDataProp = false, useClientDirective = false, defaultExport = false, ssrDataOption = false, includeOverrides = false, enableDevTools = false, useFrameworkProps, includeTheme = false, docsSlug }) {
368
+ const scriptsImport = generateScriptsImport(selectedScripts);
369
+ const scriptsConfig = selectedScripts.length ? generateScriptsConfig(selectedScripts) : generateScriptsCommentPlaceholder();
370
+ const ssrDataLine = ssrDataOption ? '\n\t\t\t\tssrData,' : '';
371
+ const themeLine = includeTheme ? '\n\t\t\t\ttheme,' : '';
372
+ const overridesLine = includeOverrides ? `\n\t\t\t\t// Shows banner during development. Remove for production.\n\t\t\t\toverrides: { country: 'DE' },` : '';
373
+ const fullOptionsText = `{
374
+ ${optionsText}${ssrDataLine}${themeLine}
375
+ ${scriptsConfig}${overridesLine}
376
+ }`;
377
+ const useConsentManagerProps = useFrameworkProps && ssrDataOption;
378
+ const needsDataType = (initialDataProp || ssrDataOption) && !useConsentManagerProps;
379
+ const namedImports = needsDataType ? `ConsentDialog,
380
+ ConsentManagerProvider,
381
+ ConsentBanner,
382
+ type InitialDataPromise` : `ConsentDialog,
383
+ ConsentManagerProvider,
384
+ ConsentBanner,`;
385
+ const frameworkPropsImport = useConsentManagerProps ? `import type { ConsentManagerProps } from '${useFrameworkProps}';\n` : '';
386
+ let propsDestructure;
387
+ propsDestructure = useConsentManagerProps ? "{ children, ssrData }: ConsentManagerProps" : ssrDataOption ? `{
388
+ children,
389
+ ssrData,
390
+ }: {
391
+ children: ReactNode;
392
+ ssrData?: InitialDataPromise;
393
+ }` : initialDataProp ? `{
394
+ children,
395
+ initialData,
396
+ }: {
397
+ children: ReactNode;
398
+ initialData?: InitialDataPromise;
399
+ }` : '{ children }: { children: ReactNode }';
400
+ const providerProps = initialDataProp ? `\n\t\t\tinitialData={initialData}\n\t\t\toptions={${fullOptionsText}}` : ` options={${fullOptionsText}}`;
401
+ const directive = useClientDirective ? "'use client';\n\n" : '';
402
+ const devToolsImport = enableDevTools ? "import { DevTools } from '@c15t/dev-tools/react';\n" : '';
403
+ const themeImport = includeTheme ? "import { theme } from './theme';\n" : '';
404
+ const componentName = defaultExport ? 'ConsentManagerClient' : 'ConsentManager';
405
+ const exportPrefix = defaultExport ? 'export default function' : 'export function';
406
+ const docComment = buildDocComment({
407
+ defaultExport,
408
+ initialDataProp,
409
+ ssrDataOption,
410
+ docsSlug
411
+ });
412
+ const preDocComment = initialDataProp ? `// For client-only apps (non-SSR), you can use:
413
+ // import { ConsentManagerProvider } from '@c15t/nextjs/client';
414
+
415
+ ` : '';
416
+ return `${directive}import type { ReactNode } from 'react';
417
+ import {
418
+ ${namedImports}
419
+ } from '${importSource}';
420
+ ${frameworkPropsImport}${devToolsImport}${themeImport}${scriptsImport ? `${scriptsImport}\n` : ''}${preDocComment}${docComment}
421
+ ${exportPrefix} ${componentName}(${propsDestructure}) {
422
+ return (
423
+ <ConsentManagerProvider${providerProps}>
424
+ <ConsentBanner />
425
+ <ConsentDialog />
426
+ ${enableDevTools ? "<DevTools disabled={process.env.NODE_ENV === 'production'} />" : ''}
427
+ {children}
428
+ </ConsentManagerProvider>
429
+ );
430
+ }
431
+ `;
432
+ }
433
+ function buildDocComment({ defaultExport, initialDataProp, ssrDataOption, docsSlug }) {
434
+ if (defaultExport) {
435
+ const slug = docsSlug || 'nextjs';
436
+ return `/**
437
+ * Client-side consent manager provider.
438
+ * @see https://v2.c15t.com/docs/frameworks/${slug}/quickstart
439
+ */`;
440
+ }
441
+ if (initialDataProp) return `/**
442
+ * Consent management wrapper for Next.js Pages Router.
443
+ * @see https://v2.c15t.com/docs/frameworks/nextjs/quickstart
444
+ */`;
445
+ const slug = docsSlug || 'react';
446
+ return `/**
447
+ * Consent manager provider.
448
+ * @see https://v2.c15t.com/docs/frameworks/${slug}/quickstart
449
+ */`;
450
+ }
451
+ async function directory_getComponentsDirectory(projectRoot, sourceDir) {
452
+ const isSrcDir = 'src' === sourceDir || sourceDir.startsWith('src');
453
+ const candidates = isSrcDir ? [
454
+ 'src/components',
455
+ 'components'
456
+ ] : [
457
+ 'components',
458
+ 'src/components'
459
+ ];
460
+ for (const candidate of candidates)try {
461
+ const candidatePath = node_path.join(projectRoot, candidate);
462
+ const stat = await promises.stat(candidatePath);
463
+ if (stat.isDirectory()) return candidate;
464
+ } catch {}
465
+ return isSrcDir ? 'src/components' : 'components';
466
+ }
467
+ function getFrameworkDirectory(filePath, dirName) {
468
+ const normalizedPath = node_path.normalize(filePath);
469
+ const srcSegment = node_path.join('src', dirName);
470
+ if (normalizedPath.includes(srcSegment)) return node_path.join('src', dirName);
471
+ return dirName;
472
+ }
473
+ function getSourceDirectory(filePath) {
474
+ const normalizedPath = node_path.normalize(filePath);
475
+ const segments = normalizedPath.split(node_path.sep);
476
+ if (segments.includes('src')) return 'src';
477
+ return '';
478
+ }
479
+ function generateExpandedProviderTemplate({ enableSSR, enableDevTools, optionsText, framework }) {
480
+ const useConsentManagerProps = enableSSR && framework.hasSSRProps;
481
+ let propsInterface;
482
+ let propsDestructure;
483
+ let typeImports;
484
+ if (useConsentManagerProps) {
485
+ propsInterface = '';
486
+ propsDestructure = '{ children, ssrData }: ConsentManagerProps';
487
+ typeImports = `import type { ConsentManagerProps } from '${framework.importSource}';`;
488
+ } else if (enableSSR) {
489
+ propsInterface = `\ninterface Props {
490
+ children: ReactNode;
491
+ ssrData?: InitialDataPromise;
492
+ }\n`;
493
+ propsDestructure = '{ children, ssrData }: Props';
494
+ typeImports = `import type { InitialDataPromise } from '${framework.importSource}';`;
495
+ } else {
496
+ propsInterface = `\ninterface Props {
497
+ children: ReactNode;
498
+ }\n`;
499
+ propsDestructure = '{ children }: Props';
500
+ typeImports = '';
501
+ }
502
+ const ssrDataOption = enableSSR ? '\n\t\t\t\tssrData,' : '';
503
+ const devToolsImport = enableDevTools ? "import { DevTools } from '@c15t/dev-tools/react';\n" : '';
504
+ const reactNodeImport = useConsentManagerProps ? '' : "import type { ReactNode } from 'react';\n";
505
+ return `'use client';
506
+
507
+ ${reactNodeImport}import { ConsentManagerProvider } from '${framework.importSource}';
508
+ ${typeImports}
509
+ ${devToolsImport}import ConsentBanner from './consent-banner';
510
+ import ConsentDialog from './consent-dialog';
511
+ import { theme } from './theme';
512
+ ${propsInterface}
513
+ /**
514
+ * Client-side consent manager provider with compound components.
515
+ * @see https://v2.c15t.com/docs/frameworks/${framework.docsSlug}/quickstart
516
+ */
517
+ export default function ConsentManagerClient(${propsDestructure}) {
518
+ return (
519
+ <ConsentManagerProvider
520
+ options={{
521
+ ${optionsText}${ssrDataOption}
522
+ theme,
523
+ // Add your scripts here:
524
+ // scripts: [
525
+ // googleTagManager({ id: 'GTM-XXXXXX' }),
526
+ // ],${!enableSSR ? "\n\t\t\t\t// Shows banner during development. Remove for production.\n\t\t\t\toverrides: { country: 'DE' }," : ''}
527
+ }}
528
+ >
529
+ <ConsentBanner />
530
+ <ConsentDialog />
531
+ ${enableDevTools ? "<DevTools disabled={process.env.NODE_ENV === 'production'} />" : ''}
532
+ {children}
533
+ </ConsentManagerProvider>
534
+ );
535
+ }
536
+ `;
537
+ }
538
+ function generateExpandedConsentDialogTemplate(framework) {
539
+ return `'use client';
540
+
541
+ import { ConsentWidget } from '${framework.importSource}';
542
+ import { ConsentDialog } from '${framework.consentDialogImport}';
543
+
544
+ /**
545
+ * Consent dialog using compound components.
546
+ * @see https://v2.c15t.com/docs/frameworks/${framework.docsSlug}/components/consent-dialog
547
+ */
548
+ export default function () {
549
+ return (
550
+ <ConsentDialog.Root>
551
+ <ConsentDialog.Card>
552
+ <ConsentDialog.Header>
553
+ <ConsentDialog.HeaderTitle />
554
+ <ConsentDialog.HeaderDescription />
555
+ </ConsentDialog.Header>
556
+ <ConsentDialog.Content>
557
+ <ConsentWidget />
558
+ </ConsentDialog.Content>
559
+ <ConsentDialog.Footer />
560
+ </ConsentDialog.Card>
561
+ </ConsentDialog.Root>
562
+ );
563
+ }
564
+ `;
565
+ }
566
+ function generateExpandedConsentBannerTemplate(framework) {
567
+ return `'use client';
568
+
569
+ import { ConsentBanner } from '${framework.consentBannerImport}';
570
+
571
+ /**
572
+ * Consent banner using compound components.
573
+ * @see https://v2.c15t.com/docs/frameworks/${framework.docsSlug}/components/consent-banner
574
+ */
575
+ export default function () {
576
+ return (
577
+ <ConsentBanner.Root>
578
+ <ConsentBanner.Card>
579
+ <ConsentBanner.Header>
580
+ <ConsentBanner.Title />
581
+ <ConsentBanner.Description
582
+ legalLinks={['privacyPolicy', 'termsOfService']}
583
+ />
584
+ </ConsentBanner.Header>
585
+ <ConsentBanner.Footer>
586
+ <ConsentBanner.FooterSubGroup>
587
+ <ConsentBanner.RejectButton />
588
+ <ConsentBanner.AcceptButton />
589
+ </ConsentBanner.FooterSubGroup>
590
+ <ConsentBanner.CustomizeButton />
591
+ </ConsentBanner.Footer>
592
+ </ConsentBanner.Card>
593
+ </ConsentBanner.Root>
594
+ );
595
+ }
596
+ `;
597
+ }
598
+ function generateExpandedThemeTemplate(theme, framework) {
599
+ switch(theme){
600
+ case 'tailwind':
601
+ return generateTailwindTheme(framework);
602
+ case 'minimal':
603
+ return generateMinimalTheme(framework);
604
+ case 'dark':
605
+ return generateDarkTheme(framework);
606
+ default:
607
+ return generateTailwindTheme(framework);
608
+ }
609
+ }
610
+ function generateTailwindTheme(framework) {
611
+ return `import type { Theme } from '${framework.importSource}';
612
+
613
+ /**
614
+ * Tailwind Theme
615
+ *
616
+ * Uses standard Tailwind colors (Slate/Blue) with backdrop blur effects.
617
+ * This theme works well with Tailwind CSS projects.
618
+ *
619
+ * Customize the colors, typography, and slots below to match your design.
620
+ *
621
+ * @see https://v2.c15t.com/docs/customization/theming
622
+ */
623
+ export const theme: Theme = {
624
+ colors: {
625
+ primary: '#3b82f6', // blue-500
626
+ primaryHover: '#2563eb', // blue-600
627
+ surface: '#ffffff',
628
+ surfaceHover: '#f8fafc', // slate-50
629
+ border: '#e2e8f0', // slate-200
630
+ borderHover: '#cbd5e1', // slate-300
631
+ text: '#0f172a', // slate-900
632
+ textMuted: '#64748b', // slate-500
633
+ textOnPrimary: '#ffffff',
634
+ switchTrack: '#e2e8f0',
635
+ switchTrackActive: '#3b82f6',
636
+ switchThumb: '#ffffff',
637
+ },
638
+ typography: {
639
+ fontFamily: 'ui-sans-serif, system-ui, -apple-system, sans-serif',
640
+ },
641
+ radius: {
642
+ sm: '0.125rem',
643
+ md: '0.375rem',
644
+ lg: '0.5rem',
645
+ full: '9999px',
646
+ },
647
+ slots: {
648
+ consentBannerCard:
649
+ 'border border-slate-200 bg-white/95 backdrop-blur-sm shadow-md',
650
+ consentDialogCard:
651
+ 'border border-slate-200 bg-white/95 backdrop-blur-md shadow-xl',
652
+ buttonPrimary:
653
+ 'bg-blue-600 text-white hover:bg-blue-700 shadow-sm transition-colors',
654
+ buttonSecondary:
655
+ 'bg-white text-slate-600 border border-slate-200 hover:bg-slate-50 transition-colors',
656
+ consentBannerTitle: 'text-slate-900 font-semibold',
657
+ consentBannerDescription: 'text-slate-500',
658
+ },
659
+ };
660
+ `;
661
+ }
662
+ function generateMinimalTheme(framework) {
663
+ return `import type { Theme } from '${framework.importSource}';
664
+
665
+ /**
666
+ * Minimal Theme
667
+ *
668
+ * A clean, light theme with subtle grays and refined typography.
669
+ * Uses standard CSS (no Tailwind dependency).
670
+ *
671
+ * Customize the colors, typography, and slots below to match your design.
672
+ *
673
+ * @see https://v2.c15t.com/docs/customization/theming
674
+ */
675
+ export const theme: Theme = {
676
+ colors: {
677
+ primary: '#18181b',
678
+ primaryHover: '#27272a',
679
+ surface: '#ffffff',
680
+ surfaceHover: '#fafafa',
681
+ border: '#e4e4e7',
682
+ borderHover: '#d4d4d8',
683
+ text: '#18181b',
684
+ textMuted: '#71717a',
685
+ textOnPrimary: '#ffffff',
686
+ switchTrack: '#d4d4d8',
687
+ switchTrackActive: '#18181b',
688
+ switchThumb: '#ffffff',
689
+ },
690
+ dark: {
691
+ primary: '#fafafa',
692
+ primaryHover: '#e4e4e7',
693
+ surface: '#0a0a0a',
694
+ surfaceHover: '#171717',
695
+ border: '#27272a',
696
+ borderHover: '#3f3f46',
697
+ text: '#fafafa',
698
+ textMuted: '#a1a1aa',
699
+ textOnPrimary: '#09090b',
700
+ },
701
+ typography: {
702
+ fontFamily: 'var(--font-inter), system-ui, sans-serif',
703
+ fontSize: {
704
+ sm: '0.8125rem',
705
+ base: '0.875rem',
706
+ lg: '1rem',
707
+ },
708
+ fontWeight: {
709
+ normal: 400,
710
+ medium: 500,
711
+ semibold: 500,
712
+ },
713
+ lineHeight: {
714
+ tight: '1.3',
715
+ normal: '1.5',
716
+ relaxed: '1.7',
717
+ },
718
+ },
719
+ radius: {
720
+ sm: '0.25rem',
721
+ md: '0.375rem',
722
+ lg: '0.5rem',
723
+ full: '9999px',
724
+ },
725
+ shadows: {
726
+ sm: '0 1px 2px rgba(0, 0, 0, 0.04)',
727
+ md: '0 2px 8px rgba(0, 0, 0, 0.06)',
728
+ lg: '0 4px 16px rgba(0, 0, 0, 0.08)',
729
+ },
730
+ slots: {
731
+ consentBannerCard: {
732
+ style: {
733
+ border: '1px solid var(--c15t-border)',
734
+ boxShadow: 'var(--c15t-shadow-sm)',
735
+ },
736
+ },
737
+ consentDialogCard: {
738
+ style: {
739
+ border: '1px solid var(--c15t-border)',
740
+ boxShadow: 'var(--c15t-shadow-lg)',
741
+ },
742
+ },
743
+ buttonPrimary: {
744
+ style: {
745
+ borderRadius: 'var(--c15t-radius-sm)',
746
+ boxShadow: 'none',
747
+ fontWeight: 500,
748
+ },
749
+ },
750
+ buttonSecondary: {
751
+ style: {
752
+ borderRadius: 'var(--c15t-radius-sm)',
753
+ backgroundColor: 'transparent',
754
+ border: '1px solid var(--c15t-border)',
755
+ color: 'var(--c15t-text-muted)',
756
+ boxShadow: 'none',
757
+ fontWeight: 500,
758
+ },
759
+ },
760
+ },
761
+ };
762
+ `;
763
+ }
764
+ function generateDarkTheme(framework) {
765
+ return `import type { Theme } from '${framework.importSource}';
766
+
767
+ /**
768
+ * Dark Mode Theme
769
+ *
770
+ * High contrast black and white theme.
771
+ * Stays dark regardless of system preference.
772
+ * Uses standard CSS (no Tailwind dependency).
773
+ *
774
+ * Customize the colors, typography, and slots below to match your design.
775
+ *
776
+ * @see https://v2.c15t.com/docs/customization/theming
777
+ */
778
+ export const theme: Theme = {
779
+ colors: {
780
+ // Define dark colors as the default to enforce dark mode
781
+ primary: '#ffffff',
782
+ primaryHover: '#ededed',
783
+ surface: '#000000',
784
+ surfaceHover: '#111111',
785
+ border: '#333333',
786
+ borderHover: '#444444',
787
+ text: '#ffffff',
788
+ textMuted: '#888888',
789
+ textOnPrimary: '#000000',
790
+ switchTrack: '#333333',
791
+ switchTrackActive: '#ffffff',
792
+ switchThumb: '#000000',
793
+ },
794
+ // No 'dark' overrides needed as the base IS dark
795
+ typography: {
796
+ fontFamily: 'var(--font-inter), system-ui, sans-serif',
797
+ fontSize: {
798
+ sm: '0.8125rem',
799
+ base: '0.875rem',
800
+ lg: '1rem',
801
+ },
802
+ fontWeight: {
803
+ normal: 400,
804
+ medium: 500,
805
+ semibold: 600,
806
+ },
807
+ },
808
+ radius: {
809
+ sm: '0.25rem',
810
+ md: '0.375rem',
811
+ lg: '0.5rem',
812
+ full: '9999px',
813
+ },
814
+ shadows: {
815
+ sm: '0 1px 2px rgba(255, 255, 255, 0.1)',
816
+ md: '0 4px 8px rgba(0, 0, 0, 0.5)',
817
+ lg: '0 8px 16px rgba(0, 0, 0, 0.5)',
818
+ },
819
+ slots: {
820
+ consentBannerCard: {
821
+ style: {
822
+ backgroundColor: '#000000',
823
+ border: '1px solid #333333',
824
+ boxShadow: 'none',
825
+ },
826
+ },
827
+ consentDialogCard: {
828
+ style: {
829
+ backgroundColor: '#000000',
830
+ border: '1px solid #333333',
831
+ boxShadow: '0 0 0 1px #333333, 0 8px 40px rgba(0,0,0,0.5)',
832
+ },
833
+ },
834
+ buttonPrimary: {
835
+ style: {
836
+ backgroundColor: '#ffffff',
837
+ color: '#000000',
838
+ border: '1px solid #ffffff',
839
+ boxShadow: 'none',
840
+ fontWeight: 500,
841
+ },
842
+ },
843
+ buttonSecondary: {
844
+ style: {
845
+ backgroundColor: '#000000',
846
+ border: '1px solid #333333',
847
+ color: '#888888',
848
+ boxShadow: 'none',
849
+ fontWeight: 500,
850
+ },
851
+ },
852
+ },
853
+ };
854
+ `;
855
+ }
856
+ const NEXTJS_CONFIG = {
857
+ importSource: '@c15t/nextjs',
858
+ consentBannerImport: '@c15t/nextjs',
859
+ consentDialogImport: '@c15t/nextjs',
860
+ frameworkName: 'Next.js App Router',
861
+ ssrMechanism: 'Next.js headers() API',
862
+ docsSlug: 'nextjs',
863
+ envVarPrefix: 'NEXT_PUBLIC',
864
+ hasSSRProps: true,
865
+ stylesheetImport: '@c15t/nextjs/styles.css',
866
+ iabStylesheetImport: '@c15t/nextjs/iab/styles.css'
867
+ };
868
+ const REACT_CONFIG = {
869
+ importSource: '@c15t/react',
870
+ consentBannerImport: '@c15t/react',
871
+ consentDialogImport: '@c15t/react',
872
+ frameworkName: 'React',
873
+ ssrMechanism: '',
874
+ docsSlug: 'react',
875
+ envVarPrefix: '',
876
+ hasSSRProps: false,
877
+ stylesheetImport: '@c15t/react/styles.css',
878
+ iabStylesheetImport: '@c15t/react/iab/styles.css'
879
+ };
880
+ function computeRelativeModuleSpecifier(fromFilePath, toFilePath) {
881
+ const fromDir = node_path.dirname(fromFilePath);
882
+ let relativePath = node_path.relative(fromDir, toFilePath);
883
+ relativePath = relativePath.split(node_path.sep).join('/');
884
+ relativePath = relativePath.replace(/\.(tsx?|jsx?)$/, '');
885
+ relativePath = relativePath.replace(/\/index$/, '');
886
+ if (!relativePath.startsWith('.')) relativePath = `./${relativePath}`;
887
+ return relativePath;
888
+ }
889
+ function hasConsentManagerImport(sourceFile, moduleSpecifier) {
890
+ const existingImports = sourceFile.getImportDeclarations();
891
+ return existingImports.some((importDecl)=>{
892
+ const spec = importDecl.getModuleSpecifierValue();
893
+ return './consent-manager' === spec || './consent-manager.tsx' === spec || spec.endsWith('/consent-manager') || spec.endsWith('/consent-manager/index') || void 0 !== moduleSpecifier && spec === moduleSpecifier;
894
+ });
895
+ }
896
+ function addConsentManagerImport(sourceFile, consentManagerFilePath) {
897
+ const sourceFilePath = sourceFile.getFilePath();
898
+ const moduleSpecifier = computeRelativeModuleSpecifier(sourceFilePath, consentManagerFilePath);
899
+ if (!hasConsentManagerImport(sourceFile, moduleSpecifier)) sourceFile.addImportDeclaration({
900
+ namedImports: [
901
+ 'ConsentManager'
902
+ ],
903
+ moduleSpecifier
904
+ });
905
+ }
906
+ async function runLayoutUpdatePipeline(config) {
907
+ const { filePatterns, projectRoot, knownFilePath, frameworkDirName, createComponents, wrapJsx, afterImport } = config;
908
+ const project = new Project();
909
+ let layoutFile;
910
+ if (knownFilePath) try {
911
+ layoutFile = project.addSourceFileAtPath(knownFilePath);
912
+ } catch {}
913
+ else for (const pattern of filePatterns){
914
+ const files = project.addSourceFilesAtPaths(`${projectRoot}/${pattern}`);
915
+ if (files.length > 0) {
916
+ layoutFile = files[0];
917
+ break;
918
+ }
919
+ }
920
+ if (!layoutFile) return {
921
+ updated: false,
922
+ filePath: null,
923
+ alreadyModified: false
924
+ };
925
+ const layoutFilePath = layoutFile.getFilePath();
926
+ const frameworkDir = getFrameworkDirectory(layoutFilePath, frameworkDirName);
927
+ if (hasConsentManagerImport(layoutFile)) return {
928
+ updated: false,
929
+ filePath: layoutFilePath,
930
+ alreadyModified: true
931
+ };
932
+ const componentFiles = await createComponents(layoutFilePath, frameworkDir);
933
+ addConsentManagerImport(layoutFile, componentFiles.consentManager);
934
+ if (afterImport) afterImport(layoutFile);
935
+ const returnStatement = layoutFile.getDescendantsOfKind(SyntaxKind.ReturnStatement)[0];
936
+ if (!returnStatement) return {
937
+ updated: false,
938
+ filePath: layoutFilePath,
939
+ alreadyModified: false
940
+ };
941
+ const expression = returnStatement.getExpression();
942
+ if (!expression) return {
943
+ updated: false,
944
+ filePath: layoutFilePath,
945
+ alreadyModified: false
946
+ };
947
+ const originalJsx = expression.getText();
948
+ const newJsx = wrapJsx(originalJsx);
949
+ returnStatement.replaceWithText(`return ${newJsx}`);
950
+ await layoutFile.save();
951
+ return {
952
+ updated: true,
953
+ filePath: layoutFilePath,
954
+ alreadyModified: false,
955
+ componentFiles
956
+ };
957
+ }
958
+ function getBackendURLValue(backendURL, useEnvFile, proxyNextjs, envVarPrefix = 'NEXT_PUBLIC') {
959
+ if (proxyNextjs) return '"/api/c15t"';
960
+ if (useEnvFile) return `process.env.${envVarPrefix}_C15T_URL!`;
961
+ return `'${backendURL || 'https://your-instance.c15t.dev'}'`;
962
+ }
963
+ function generateOptionsText(mode, backendURL, useEnvFile, proxyNextjs, inlineCustomHandlers, envVarPrefix = 'NEXT_PUBLIC') {
964
+ switch(mode){
965
+ case 'hosted':
966
+ case 'c15t':
967
+ case 'self-hosted':
968
+ {
969
+ const backendURLValue = getBackendURLValue(backendURL, useEnvFile, proxyNextjs, envVarPrefix);
970
+ return `mode: 'hosted',
971
+ backendURL: ${backendURLValue},`;
972
+ }
973
+ case 'custom':
974
+ if (inlineCustomHandlers) {
975
+ const url = useEnvFile ? `process.env.${envVarPrefix}_CONSENT_API_URL` : `'${backendURL || '/api/consent'}'`;
976
+ return `mode: 'custom',
977
+ endpointHandlers: {
978
+ async getConsent() {
979
+ const res = await fetch(${url});
980
+ return res.json();
981
+ },
982
+ async setConsent(consent) {
983
+ const res = await fetch(${url}, {
984
+ method: 'POST',
985
+ headers: { 'Content-Type': 'application/json' },
986
+ body: JSON.stringify(consent),
987
+ });
988
+ return res.json();
989
+ },
990
+ },`;
991
+ }
992
+ return `mode: 'custom',
993
+ endpointHandlers: createCustomHandlers(),`;
994
+ default:
995
+ return "mode: 'offline',";
996
+ }
997
+ }
998
+ function generateServerComponent({ enableSSR, backendURLValue, framework }) {
999
+ if (enableSSR) return `import { fetchInitialData } from '${framework.importSource}';
1000
+ import type { ReactNode } from 'react';
1001
+ import ConsentManagerProvider from './provider';
1002
+
1003
+ /**
1004
+ * Server-side consent management wrapper with SSR data prefetching.
1005
+ * @see https://v2.c15t.com/docs/frameworks/${framework.docsSlug}/quickstart
1006
+ */
1007
+ export function ConsentManager({ children }: { children: ReactNode }) {
1008
+ const ssrData = fetchInitialData({
1009
+ backendURL: ${backendURLValue},
1010
+ // Shows banner during development. Remove for production.
1011
+ overrides: { country: 'DE' },
1012
+ });
1013
+
1014
+ return (
1015
+ <ConsentManagerProvider ssrData={ssrData}>
1016
+ {children}
1017
+ </ConsentManagerProvider>
1018
+ );
1019
+ }
1020
+ `;
1021
+ return `import type { ReactNode } from 'react';
1022
+ import ConsentManagerProvider from './provider';
1023
+
1024
+ /**
1025
+ * Consent management wrapper.
1026
+ * @see https://v2.c15t.com/docs/frameworks/${framework.docsSlug}/quickstart
1027
+ */
1028
+ export function ConsentManager({ children }: { children: ReactNode }) {
1029
+ return (
1030
+ <ConsentManagerProvider>
1031
+ {children}
1032
+ </ConsentManagerProvider>
1033
+ );
1034
+ }
1035
+ `;
1036
+ }
1037
+ function generateSimpleWrapperComponent(_frameworkName, docsSlug) {
1038
+ return `import type { ReactNode } from 'react';
1039
+ import ConsentManagerProvider from './provider';
1040
+
1041
+ /**
1042
+ * Consent management wrapper.
1043
+ * @see https://v2.c15t.com/docs/frameworks/${docsSlug}/quickstart
1044
+ */
1045
+ export function ConsentManager({ children }: { children: ReactNode }) {
1046
+ return <ConsentManagerProvider>{children}</ConsentManagerProvider>;
1047
+ }
1048
+ `;
1049
+ }
1050
+ const HTML_TAG_REGEX = /<html[^>]*>([\s\S]*)<\/html>/;
1051
+ const BODY_TAG_REGEX = /<body[^>]*>([\s\S]*)<\/body>/;
1052
+ const BODY_OPENING_TAG_REGEX = /<body[^>]*>/;
1053
+ const HTML_CONTENT_REGEX = /([\s\S]*<\/html>)/;
1054
+ function wrapAppJsxContent(originalJsx) {
1055
+ const hasHtmlTag = originalJsx.includes('<html') || originalJsx.includes('</html>');
1056
+ const hasBodyTag = originalJsx.includes('<body') || originalJsx.includes('</body>');
1057
+ const consentWrapper = (content)=>`
1058
+ <ConsentManager>
1059
+ ${content}
1060
+ </ConsentManager>
1061
+ `;
1062
+ if (hasHtmlTag) {
1063
+ const htmlMatch = originalJsx.match(HTML_TAG_REGEX);
1064
+ const htmlContent = htmlMatch?.[1] || '';
1065
+ if (!htmlContent) return consentWrapper(originalJsx);
1066
+ const bodyMatch = htmlContent.match(BODY_TAG_REGEX);
1067
+ if (!bodyMatch) return originalJsx.replace(HTML_CONTENT_REGEX, `<html>${consentWrapper('$1')}</html>`);
1068
+ const bodyContent = bodyMatch[1] || '';
1069
+ const bodyOpeningTag = originalJsx.match(BODY_OPENING_TAG_REGEX)?.[0] || '<body>';
1070
+ return originalJsx.replace(BODY_TAG_REGEX, `${bodyOpeningTag}${consentWrapper(bodyContent)}</body>`);
1071
+ }
1072
+ if (hasBodyTag) {
1073
+ const bodyMatch = originalJsx.match(BODY_TAG_REGEX);
1074
+ const bodyContent = bodyMatch?.[1] || '';
1075
+ if (!bodyContent) return consentWrapper(originalJsx);
1076
+ const bodyOpeningTag = originalJsx.match(BODY_OPENING_TAG_REGEX)?.[0] || '<body>';
1077
+ return originalJsx.replace(BODY_TAG_REGEX, `${bodyOpeningTag}${consentWrapper(bodyContent)}</body>`);
1078
+ }
1079
+ return consentWrapper(originalJsx);
1080
+ }
1081
+ async function createExpandedConsentManagerComponents(projectRoot, appDir, options) {
1082
+ const { mode, backendURL, useEnvFile, proxyNextjs, enableSSR, enableDevTools, expandedTheme } = options;
1083
+ const componentsDir = await directory_getComponentsDirectory(projectRoot, appDir);
1084
+ const consentManagerDirPath = node_path.join(projectRoot, componentsDir, 'consent-manager');
1085
+ const backendURLValue = getBackendURLValue(backendURL, useEnvFile, proxyNextjs, NEXTJS_CONFIG.envVarPrefix);
1086
+ const optionsText = generateOptionsText(mode, backendURL, useEnvFile, proxyNextjs, void 0, NEXTJS_CONFIG.envVarPrefix);
1087
+ const serverComponentContent = generateServerComponent({
1088
+ enableSSR,
1089
+ backendURLValue,
1090
+ framework: NEXTJS_CONFIG
1091
+ });
1092
+ const providerContent = generateExpandedProviderTemplate({
1093
+ enableSSR,
1094
+ enableDevTools: Boolean(enableDevTools),
1095
+ optionsText,
1096
+ framework: NEXTJS_CONFIG
1097
+ });
1098
+ const consentBannerContent = generateExpandedConsentBannerTemplate(NEXTJS_CONFIG);
1099
+ const consentDialogContent = generateExpandedConsentDialogTemplate(NEXTJS_CONFIG);
1100
+ const themeContent = generateExpandedThemeTemplate(expandedTheme, NEXTJS_CONFIG);
1101
+ const indexPath = node_path.join(consentManagerDirPath, 'index.tsx');
1102
+ const providerPath = node_path.join(consentManagerDirPath, 'provider.tsx');
1103
+ const consentBannerPath = node_path.join(consentManagerDirPath, 'consent-banner.tsx');
1104
+ const consentDialogPath = node_path.join(consentManagerDirPath, 'consent-dialog.tsx');
1105
+ const themePath = node_path.join(consentManagerDirPath, 'theme.ts');
1106
+ await promises.mkdir(consentManagerDirPath, {
1107
+ recursive: true
1108
+ });
1109
+ await Promise.all([
1110
+ promises.writeFile(indexPath, serverComponentContent, 'utf-8'),
1111
+ promises.writeFile(providerPath, providerContent, 'utf-8'),
1112
+ promises.writeFile(consentBannerPath, consentBannerContent, 'utf-8'),
1113
+ promises.writeFile(consentDialogPath, consentDialogContent, 'utf-8'),
1114
+ promises.writeFile(themePath, themeContent, 'utf-8')
1115
+ ]);
1116
+ return {
1117
+ consentManager: indexPath,
1118
+ consentManagerDir: consentManagerDirPath
1119
+ };
1120
+ }
1121
+ async function createPrebuiltConsentManagerComponents(projectRoot, appDir, options) {
1122
+ const { mode, backendURL, useEnvFile, proxyNextjs, enableSSR, enableDevTools, expandedTheme, selectedScripts } = options;
1123
+ const hasTheme = expandedTheme && 'none' !== expandedTheme;
1124
+ const componentsDir = await directory_getComponentsDirectory(projectRoot, appDir);
1125
+ const consentManagerDirPath = node_path.join(projectRoot, componentsDir, 'consent-manager');
1126
+ const backendURLValue = getBackendURLValue(backendURL, useEnvFile, proxyNextjs, NEXTJS_CONFIG.envVarPrefix);
1127
+ const optionsText = generateOptionsText(mode, backendURL, useEnvFile, proxyNextjs, void 0, NEXTJS_CONFIG.envVarPrefix);
1128
+ const consentManagerContent = generateServerComponent({
1129
+ enableSSR,
1130
+ backendURLValue,
1131
+ framework: NEXTJS_CONFIG
1132
+ });
1133
+ const consentManagerClientContent = generateConsentComponent({
1134
+ importSource: NEXTJS_CONFIG.importSource,
1135
+ optionsText,
1136
+ selectedScripts,
1137
+ useClientDirective: true,
1138
+ defaultExport: true,
1139
+ ssrDataOption: enableSSR,
1140
+ includeOverrides: !enableSSR,
1141
+ enableDevTools: Boolean(enableDevTools),
1142
+ useFrameworkProps: enableSSR ? NEXTJS_CONFIG.importSource : void 0,
1143
+ includeTheme: Boolean(hasTheme),
1144
+ docsSlug: NEXTJS_CONFIG.docsSlug
1145
+ });
1146
+ const indexPath = node_path.join(consentManagerDirPath, 'index.tsx');
1147
+ const providerPath = node_path.join(consentManagerDirPath, 'provider.tsx');
1148
+ await promises.mkdir(consentManagerDirPath, {
1149
+ recursive: true
1150
+ });
1151
+ const writePromises = [
1152
+ promises.writeFile(indexPath, consentManagerContent, 'utf-8'),
1153
+ promises.writeFile(providerPath, consentManagerClientContent, 'utf-8')
1154
+ ];
1155
+ if (hasTheme) {
1156
+ const themeContent = generateExpandedThemeTemplate(expandedTheme, NEXTJS_CONFIG);
1157
+ const themePath = node_path.join(consentManagerDirPath, 'theme.ts');
1158
+ writePromises.push(promises.writeFile(themePath, themeContent, 'utf-8'));
1159
+ }
1160
+ await Promise.all(writePromises);
1161
+ return {
1162
+ consentManager: indexPath,
1163
+ consentManagerClient: providerPath
1164
+ };
1165
+ }
1166
+ const APP_LAYOUT_PATTERNS = [
1167
+ 'app/layout.tsx',
1168
+ 'src/app/layout.tsx',
1169
+ 'app/layout.ts',
1170
+ 'src/app/layout.ts'
1171
+ ];
1172
+ async function updateAppLayout({ projectRoot, mode, backendURL, useEnvFile, proxyNextjs, enableSSR = false, enableDevTools = false, uiStyle = 'prebuilt', expandedTheme = 'tailwind', selectedScripts, layoutFilePath }) {
1173
+ return runLayoutUpdatePipeline({
1174
+ filePatterns: APP_LAYOUT_PATTERNS,
1175
+ projectRoot,
1176
+ knownFilePath: layoutFilePath,
1177
+ frameworkDirName: 'app',
1178
+ wrapJsx: wrapAppJsxContent,
1179
+ createComponents: async (_layoutFilePath, appDir)=>{
1180
+ if ('expanded' === uiStyle) return createExpandedConsentManagerComponents(projectRoot, appDir, {
1181
+ mode,
1182
+ backendURL,
1183
+ useEnvFile,
1184
+ proxyNextjs,
1185
+ enableSSR,
1186
+ enableDevTools,
1187
+ expandedTheme
1188
+ });
1189
+ return createPrebuiltConsentManagerComponents(projectRoot, appDir, {
1190
+ mode,
1191
+ backendURL,
1192
+ useEnvFile,
1193
+ proxyNextjs,
1194
+ enableSSR,
1195
+ enableDevTools,
1196
+ expandedTheme,
1197
+ selectedScripts
1198
+ });
1199
+ }
1200
+ });
1201
+ }
1202
+ function wrapPagesJsxContent(originalJsx) {
1203
+ const trimmedJsx = originalJsx.trim();
1204
+ const hasParentheses = trimmedJsx.startsWith('(') && trimmedJsx.endsWith(')');
1205
+ const cleanJsx = hasParentheses ? trimmedJsx.slice(1, -1).trim() : originalJsx;
1206
+ const wrappedContent = `
1207
+ <ConsentManager initialData={pageProps.initialC15TData}>
1208
+ ${cleanJsx}
1209
+ </ConsentManager>
1210
+ `;
1211
+ return `(${wrappedContent})`;
1212
+ }
1213
+ async function createConsentManagerComponent(projectRoot, pagesDir, optionsText, selectedScripts, enableDevTools) {
1214
+ let componentsDir;
1215
+ componentsDir = pagesDir.includes('src') ? node_path.join('src', 'components') : 'components';
1216
+ const componentsDirPath = node_path.join(projectRoot, componentsDir);
1217
+ await promises.mkdir(componentsDirPath, {
1218
+ recursive: true
1219
+ });
1220
+ const consentManagerContent = generateConsentComponent({
1221
+ importSource: '@c15t/nextjs',
1222
+ optionsText,
1223
+ selectedScripts,
1224
+ initialDataProp: true,
1225
+ enableDevTools,
1226
+ includeOverrides: true
1227
+ });
1228
+ const consentManagerPath = node_path.join(componentsDirPath, 'consent-manager.tsx');
1229
+ await promises.writeFile(consentManagerPath, consentManagerContent, 'utf-8');
1230
+ return {
1231
+ consentManager: consentManagerPath
1232
+ };
1233
+ }
1234
+ function addServerSideDataComment(appFile, backendURL, useEnvFile, proxyNextjs) {
1235
+ const existingComments = appFile.getLeadingCommentRanges();
1236
+ let urlExample;
1237
+ urlExample = proxyNextjs ? "'/api/c15t'" : useEnvFile ? 'process.env.NEXT_PUBLIC_C15T_URL!' : `'${backendURL || 'https://your-instance.c15t.dev'}'`;
1238
+ const serverSideComment = `/**
1239
+ * Note: To get the initial server-side data on other pages, add this to each page:
1240
+ *
1241
+ * import { withInitialC15TData } from '@c15t/nextjs';
1242
+ *
1243
+ * export const getServerSideProps = withInitialC15TData(${urlExample});
1244
+ *
1245
+ * This will automatically pass initialC15TData to pageProps.initialC15TData
1246
+ */`;
1247
+ const hasServerSideComment = existingComments.some((comment)=>comment.getText().includes('withInitialC15TData'));
1248
+ if (!hasServerSideComment) appFile.insertText(0, `${serverSideComment}\n\n`);
1249
+ }
1250
+ function updateAppComponentTyping(appFile) {
1251
+ const exportAssignment = appFile.getExportAssignment(()=>true);
1252
+ if (!exportAssignment) return;
1253
+ const declaration = exportAssignment.getExpression();
1254
+ if (!declaration) return;
1255
+ const text = declaration.getText();
1256
+ if (text.includes('pageProps') && !text.includes('AppProps')) {
1257
+ const hasAppPropsImport = appFile.getImportDeclarations().some((importDecl)=>'next/app' === importDecl.getModuleSpecifierValue() && importDecl.getNamedImports().some((namedImport)=>'AppProps' === namedImport.getName()));
1258
+ if (!hasAppPropsImport) appFile.addImportDeclaration({
1259
+ namedImports: [
1260
+ 'AppProps'
1261
+ ],
1262
+ moduleSpecifier: 'next/app'
1263
+ });
1264
+ }
1265
+ }
1266
+ const PAGES_APP_PATTERNS = [
1267
+ 'pages/_app.tsx',
1268
+ 'pages/_app.ts',
1269
+ 'src/pages/_app.tsx',
1270
+ 'src/pages/_app.ts'
1271
+ ];
1272
+ async function updatePagesLayout({ projectRoot, mode, backendURL, useEnvFile, proxyNextjs, enableDevTools = false, selectedScripts, layoutFilePath }) {
1273
+ const optionsText = generateOptionsText(mode, backendURL, useEnvFile, proxyNextjs);
1274
+ return runLayoutUpdatePipeline({
1275
+ filePatterns: PAGES_APP_PATTERNS,
1276
+ projectRoot,
1277
+ knownFilePath: layoutFilePath,
1278
+ frameworkDirName: 'pages',
1279
+ wrapJsx: wrapPagesJsxContent,
1280
+ createComponents: async (_layoutFilePath, pagesDir)=>createConsentManagerComponent(projectRoot, pagesDir, optionsText, selectedScripts, enableDevTools),
1281
+ afterImport: (appFile)=>{
1282
+ updateAppComponentTyping(appFile);
1283
+ addServerSideDataComment(appFile, backendURL, useEnvFile, proxyNextjs);
1284
+ }
1285
+ });
1286
+ }
1287
+ async function updateNextLayout(options) {
1288
+ const layoutDetection = await findLayoutFile(options.projectRoot);
1289
+ if (!layoutDetection) return {
1290
+ updated: false,
1291
+ filePath: null,
1292
+ alreadyModified: false,
1293
+ structureType: null
1294
+ };
1295
+ const structureType = layoutDetection.type;
1296
+ const layoutFilePath = node_path.join(options.projectRoot, layoutDetection.path);
1297
+ let result;
1298
+ result = 'app' === structureType ? await updateAppLayout({
1299
+ ...options,
1300
+ layoutFilePath
1301
+ }) : await updatePagesLayout({
1302
+ ...options,
1303
+ layoutFilePath
1304
+ });
1305
+ return {
1306
+ ...result,
1307
+ structureType
1308
+ };
1309
+ }
1310
+ function updateGenericReactJsx(layoutFile) {
1311
+ const functionDeclarations = layoutFile.getFunctions();
1312
+ const variableDeclarations = layoutFile.getVariableDeclarations();
1313
+ for (const func of functionDeclarations){
1314
+ const returnStatement = func.getDescendantsOfKind(SyntaxKind.ReturnStatement)[0];
1315
+ if (returnStatement) return wrapReturnStatementWithConsentManager(returnStatement);
1316
+ }
1317
+ for (const varDecl of variableDeclarations){
1318
+ const initializer = varDecl.getInitializer();
1319
+ if (initializer) {
1320
+ const returnStatement = initializer.getDescendantsOfKind(SyntaxKind.ReturnStatement)[0];
1321
+ if (returnStatement) return wrapReturnStatementWithConsentManager(returnStatement);
1322
+ }
1323
+ }
1324
+ return false;
1325
+ }
1326
+ function wrapReturnStatementWithConsentManager(returnStatement) {
1327
+ const expression = returnStatement.getExpression();
1328
+ if (!expression) return false;
1329
+ let originalJsx = expression.getText();
1330
+ if (originalJsx.startsWith('(') && originalJsx.endsWith(')')) originalJsx = originalJsx.slice(1, -1).trim();
1331
+ const newJsx = `(
1332
+ <ConsentManager>
1333
+ ${originalJsx}
1334
+ </ConsentManager>
1335
+ )`;
1336
+ returnStatement.replaceWithText(`return ${newJsx}`);
1337
+ return true;
1338
+ }
1339
+ async function layout_createConsentManagerComponent(projectRoot, sourceDir, mode, backendURL, useEnvFile, selectedScripts, enableDevTools, expandedTheme) {
1340
+ const hasTheme = expandedTheme && 'none' !== expandedTheme;
1341
+ const componentsDir = await directory_getComponentsDirectory(projectRoot, sourceDir);
1342
+ const consentManagerDirPath = node_path.join(projectRoot, componentsDir, 'consent-manager');
1343
+ const optionsText = generateOptionsText(mode, backendURL, useEnvFile, void 0, true);
1344
+ const providerContent = generateConsentComponent({
1345
+ importSource: '@c15t/react',
1346
+ optionsText,
1347
+ selectedScripts,
1348
+ enableDevTools,
1349
+ useClientDirective: true,
1350
+ defaultExport: true,
1351
+ includeTheme: Boolean(hasTheme),
1352
+ includeOverrides: true,
1353
+ docsSlug: 'react'
1354
+ });
1355
+ const indexContent = generateSimpleWrapperComponent('React', 'react');
1356
+ const indexPath = node_path.join(consentManagerDirPath, 'index.tsx');
1357
+ const providerPath = node_path.join(consentManagerDirPath, 'provider.tsx');
1358
+ await promises.mkdir(consentManagerDirPath, {
1359
+ recursive: true
1360
+ });
1361
+ const writePromises = [
1362
+ promises.writeFile(indexPath, indexContent, 'utf-8'),
1363
+ promises.writeFile(providerPath, providerContent, 'utf-8')
1364
+ ];
1365
+ if (hasTheme) {
1366
+ const themeContent = generateExpandedThemeTemplate(expandedTheme, REACT_CONFIG);
1367
+ const themePath = node_path.join(consentManagerDirPath, 'theme.ts');
1368
+ writePromises.push(promises.writeFile(themePath, themeContent, 'utf-8'));
1369
+ }
1370
+ await Promise.all(writePromises);
1371
+ return {
1372
+ consentManager: indexPath,
1373
+ consentManagerDir: consentManagerDirPath
1374
+ };
1375
+ }
1376
+ async function updateGenericReactLayout({ projectRoot, mode, backendURL, useEnvFile, proxyNextjs, selectedScripts, enableDevTools, expandedTheme }) {
1377
+ const layoutPatterns = [
1378
+ 'app.tsx',
1379
+ 'App.tsx',
1380
+ 'app.jsx',
1381
+ 'App.jsx',
1382
+ 'src/app.tsx',
1383
+ 'src/App.tsx',
1384
+ 'src/app.jsx',
1385
+ 'src/App.jsx',
1386
+ 'src/app/app.tsx',
1387
+ 'src/app/App.tsx',
1388
+ 'src/app/app.jsx',
1389
+ 'src/app/App.jsx'
1390
+ ];
1391
+ const project = new Project();
1392
+ let layoutFile;
1393
+ for (const pattern of layoutPatterns)try {
1394
+ const files = project.addSourceFilesAtPaths(`${projectRoot}/${pattern}`);
1395
+ if (files.length > 0) {
1396
+ layoutFile = files[0];
1397
+ break;
1398
+ }
1399
+ } catch {}
1400
+ if (!layoutFile) return {
1401
+ updated: false,
1402
+ filePath: null,
1403
+ alreadyModified: false
1404
+ };
1405
+ const layoutFilePath = layoutFile.getFilePath();
1406
+ const sourceDir = getSourceDirectory(layoutFilePath);
1407
+ if (hasConsentManagerImport(layoutFile)) return {
1408
+ updated: false,
1409
+ filePath: layoutFilePath,
1410
+ alreadyModified: true
1411
+ };
1412
+ try {
1413
+ const componentFiles = await layout_createConsentManagerComponent(projectRoot, sourceDir, mode, backendURL, useEnvFile, selectedScripts, enableDevTools, expandedTheme);
1414
+ addConsentManagerImport(layoutFile, componentFiles.consentManager);
1415
+ const updated = updateGenericReactJsx(layoutFile);
1416
+ if (updated) {
1417
+ await layoutFile.save();
1418
+ return {
1419
+ updated: true,
1420
+ filePath: layoutFilePath,
1421
+ alreadyModified: false,
1422
+ componentFiles
1423
+ };
1424
+ }
1425
+ return {
1426
+ updated: false,
1427
+ filePath: layoutFilePath,
1428
+ alreadyModified: false
1429
+ };
1430
+ } catch (error) {
1431
+ throw new Error(`Failed to update generic React layout: ${error instanceof Error ? error.message : String(error)}`);
1432
+ }
1433
+ }
1434
+ async function updateReactLayout(options) {
1435
+ if ('@c15t/nextjs' === options.pkg) {
1436
+ const nextResult = await updateNextLayout(options);
1437
+ if (nextResult.structureType) return {
1438
+ updated: nextResult.updated,
1439
+ filePath: nextResult.filePath,
1440
+ alreadyModified: nextResult.alreadyModified,
1441
+ componentFiles: nextResult.componentFiles
1442
+ };
1443
+ }
1444
+ return updateGenericReactLayout(options);
1445
+ }
1446
+ async function updateNextConfig({ projectRoot, backendURL, useEnvFile }) {
1447
+ const project = new Project();
1448
+ const configFile = findNextConfigFile(project, projectRoot);
1449
+ if (!configFile) {
1450
+ const newConfigPath = `${projectRoot}/next.config.ts`;
1451
+ const newConfig = createNewNextConfig(backendURL, useEnvFile);
1452
+ const newConfigFile = project.createSourceFile(newConfigPath, newConfig);
1453
+ await newConfigFile.save();
1454
+ return {
1455
+ updated: true,
1456
+ filePath: newConfigPath,
1457
+ alreadyModified: false,
1458
+ created: true
1459
+ };
1460
+ }
1461
+ if (hasC15tRewriteRule(configFile)) return {
1462
+ updated: false,
1463
+ filePath: configFile.getFilePath(),
1464
+ alreadyModified: true,
1465
+ created: false
1466
+ };
1467
+ const updated = await updateExistingConfig(configFile, backendURL, useEnvFile);
1468
+ if (updated) await configFile.save();
1469
+ return {
1470
+ updated,
1471
+ filePath: configFile.getFilePath(),
1472
+ alreadyModified: false,
1473
+ created: false
1474
+ };
1475
+ }
1476
+ function findNextConfigFile(project, projectRoot) {
1477
+ const configPatterns = [
1478
+ 'next.config.ts',
1479
+ 'next.config.js',
1480
+ 'next.config.mjs'
1481
+ ];
1482
+ for (const pattern of configPatterns){
1483
+ const configPath = `${projectRoot}/${pattern}`;
1484
+ try {
1485
+ const files = project.addSourceFilesAtPaths(configPath);
1486
+ if (files.length > 0) return files[0];
1487
+ } catch {}
1488
+ }
1489
+ }
1490
+ function hasC15tRewriteRule(configFile) {
1491
+ const text = configFile.getFullText();
1492
+ return text.includes('/api/c15t/') || text.includes("'/api/c15t/:path*'");
1493
+ }
1494
+ function generateRewriteDestination(backendURL, useEnvFile) {
1495
+ if (useEnvFile) return {
1496
+ destination: '${process.env.NEXT_PUBLIC_C15T_URL}/:path*',
1497
+ isTemplateLiteral: true
1498
+ };
1499
+ return {
1500
+ destination: `${backendURL || 'https://your-instance.c15t.dev'}/:path*`,
1501
+ isTemplateLiteral: false
1502
+ };
1503
+ }
1504
+ function createNewNextConfig(backendURL, useEnvFile) {
1505
+ const { destination, isTemplateLiteral } = generateRewriteDestination(backendURL, useEnvFile);
1506
+ const destinationValue = isTemplateLiteral ? `\`${destination}\`` : `'${destination}'`;
1507
+ return `import type { NextConfig } from 'next';
1508
+
1509
+ const config: NextConfig = {
1510
+ async rewrites() {
1511
+ return [
1512
+ {
1513
+ source: '/api/c15t/:path*',
1514
+ destination: ${destinationValue},
1515
+ },
1516
+ ];
1517
+ },
1518
+ };
1519
+
1520
+ export default config;
1521
+ `;
1522
+ }
1523
+ function createRewriteRule(destination, isTemplateLiteral) {
1524
+ const destinationValue = isTemplateLiteral ? `\`${destination}\`` : `'${destination}'`;
1525
+ return `{
1526
+ source: '/api/c15t/:path*',
1527
+ destination: ${destinationValue},
1528
+ }`;
1529
+ }
1530
+ function updateExistingConfig(configFile, backendURL, useEnvFile) {
1531
+ const { destination, isTemplateLiteral } = generateRewriteDestination(backendURL, useEnvFile);
1532
+ const configObject = findConfigObject(configFile);
1533
+ if (!configObject) return false;
1534
+ const rewritesProperty = configObject.getProperty('rewrites');
1535
+ if (rewritesProperty && Node.isMethodDeclaration(rewritesProperty)) return updateExistingRewrites(rewritesProperty, destination, isTemplateLiteral);
1536
+ if (rewritesProperty && Node.isPropertyAssignment(rewritesProperty)) return updatePropertyAssignmentRewrites(rewritesProperty, destination, isTemplateLiteral);
1537
+ return addNewRewritesMethod(configObject, destination, isTemplateLiteral);
1538
+ }
1539
+ function findConfigObject(configFile) {
1540
+ return findConfigFromExportDefault(configFile) || findConfigFromVariableDeclarations(configFile);
1541
+ }
1542
+ function findConfigFromExportDefault(configFile) {
1543
+ const exportDefault = configFile.getDefaultExportSymbol();
1544
+ if (!exportDefault) return;
1545
+ const declarations = exportDefault.getDeclarations();
1546
+ for (const declaration of declarations)if (Node.isExportAssignment(declaration)) {
1547
+ const result = findConfigFromExpression(declaration.getExpression(), configFile);
1548
+ if (result) return result;
1549
+ }
1550
+ }
1551
+ function findConfigFromExpression(expression, configFile) {
1552
+ if (Node.isCallExpression(expression)) return findConfigFromCallExpression(expression, configFile);
1553
+ if (Node.isObjectLiteralExpression(expression)) return expression;
1554
+ if (Node.isIdentifier(expression)) return findConfigFromIdentifier(expression.getText(), configFile);
1555
+ }
1556
+ function findConfigFromCallExpression(expression, configFile) {
1557
+ const args = expression.getArguments();
1558
+ if (0 === args.length) return;
1559
+ const firstArg = args[0];
1560
+ if (Node.isCallExpression(firstArg)) {
1561
+ const innerArgs = firstArg.getArguments();
1562
+ if (innerArgs.length > 0 && Node.isIdentifier(innerArgs[0])) return findConfigFromIdentifier(innerArgs[0].getText(), configFile);
1563
+ }
1564
+ }
1565
+ function findConfigFromIdentifier(identifierText, configFile) {
1566
+ const configVar = configFile.getVariableDeclaration(identifierText);
1567
+ const initializer = configVar?.getInitializer();
1568
+ return initializer && Node.isObjectLiteralExpression(initializer) ? initializer : void 0;
1569
+ }
1570
+ function findConfigFromVariableDeclarations(configFile) {
1571
+ const variableDeclarations = configFile.getVariableDeclarations();
1572
+ for (const varDecl of variableDeclarations){
1573
+ const typeNode = varDecl.getTypeNode();
1574
+ if (typeNode?.getText().includes('NextConfig')) {
1575
+ const initializer = varDecl.getInitializer();
1576
+ if (Node.isObjectLiteralExpression(initializer)) return initializer;
1577
+ }
1578
+ }
1579
+ }
1580
+ function updateExistingRewrites(rewritesMethod, destination, isTemplateLiteral) {
1581
+ const body = rewritesMethod.getBody();
1582
+ if (!Node.isBlock(body)) return false;
1583
+ const returnStatement = body.getStatements().find((stmt)=>Node.isReturnStatement(stmt));
1584
+ if (!returnStatement || !Node.isReturnStatement(returnStatement)) return false;
1585
+ const expression = returnStatement.getExpression();
1586
+ if (!expression || !Node.isArrayLiteralExpression(expression)) return false;
1587
+ const newRewrite = createRewriteRule(destination, isTemplateLiteral);
1588
+ const elements = expression.getElements();
1589
+ if (elements.length > 0) expression.insertElement(0, newRewrite);
1590
+ else expression.addElement(newRewrite);
1591
+ return true;
1592
+ }
1593
+ function updatePropertyAssignmentRewrites(rewritesProperty, destination, isTemplateLiteral) {
1594
+ const initializer = rewritesProperty.getInitializer();
1595
+ if (Node.isArrayLiteralExpression(initializer)) {
1596
+ const newRewrite = createRewriteRule(destination, isTemplateLiteral);
1597
+ initializer.insertElement(0, newRewrite);
1598
+ return true;
1599
+ }
1600
+ return false;
1601
+ }
1602
+ function addNewRewritesMethod(configObject, destination, isTemplateLiteral) {
1603
+ const destinationValue = isTemplateLiteral ? `\`${destination}\`` : `'${destination}'`;
1604
+ const rewritesMethod = `async rewrites() {
1605
+ return [
1606
+ {
1607
+ source: '/api/c15t/:path*',
1608
+ destination: ${destinationValue},
1609
+ },
1610
+ ];
1611
+ }`;
1612
+ configObject.addProperty(rewritesMethod);
1613
+ return true;
1614
+ }
1615
+ async function handleReactLayout(options) {
1616
+ const { projectRoot, mode, backendURL, useEnvFile, proxyNextjs, enableSSR, enableDevTools, uiStyle, expandedTheme, selectedScripts, pkg, spinner, cwd } = options;
1617
+ spinner.start('Updating layout file...');
1618
+ const layoutResult = await updateReactLayout({
1619
+ projectRoot,
1620
+ mode,
1621
+ backendURL,
1622
+ useEnvFile,
1623
+ proxyNextjs,
1624
+ enableSSR,
1625
+ enableDevTools,
1626
+ uiStyle,
1627
+ expandedTheme,
1628
+ selectedScripts,
1629
+ pkg
1630
+ });
1631
+ const spinnerMessage = ()=>{
1632
+ if (layoutResult.alreadyModified) return {
1633
+ message: 'ConsentManager is already imported. Skipped layout file update.',
1634
+ type: 'info'
1635
+ };
1636
+ if (layoutResult.updated) {
1637
+ const typedResult = layoutResult;
1638
+ if (typedResult.componentFiles) {
1639
+ const relativeConsentManager = node_path.relative(cwd, typedResult.componentFiles.consentManager);
1640
+ const relativeLayout = node_path.relative(cwd, layoutResult.filePath || '');
1641
+ if (typedResult.componentFiles.consentManagerDir) {
1642
+ const relativeConsentManagerDir = node_path.relative(cwd, typedResult.componentFiles.consentManagerDir);
1643
+ return {
1644
+ message: `Layout setup complete!\n ${picocolors.green('✓')} Created: ${picocolors.cyan(`${relativeConsentManagerDir}/`)} (expanded components)\n ${picocolors.green('✓')} Created: ${picocolors.cyan(relativeConsentManager)}\n ${picocolors.green('✓')} Updated: ${picocolors.cyan(relativeLayout)}`,
1645
+ type: 'info'
1646
+ };
1647
+ }
1648
+ if (typedResult.componentFiles.consentManagerClient) {
1649
+ const relativeConsentManagerClient = node_path.relative(cwd, typedResult.componentFiles.consentManagerClient);
1650
+ return {
1651
+ message: `Layout setup complete!\n ${picocolors.green('✓')} Created: ${picocolors.cyan(relativeConsentManager)}\n ${picocolors.green('✓')} Created: ${picocolors.cyan(relativeConsentManagerClient)}\n ${picocolors.green('✓')} Updated: ${picocolors.cyan(relativeLayout)}`,
1652
+ type: 'info'
1653
+ };
1654
+ }
1655
+ return {
1656
+ message: `Layout setup complete!\n ${picocolors.green('✓')} Created: ${picocolors.cyan(relativeConsentManager)}\n ${picocolors.green('✓')} Updated: ${picocolors.cyan(relativeLayout)}`,
1657
+ type: 'info'
1658
+ };
1659
+ }
1660
+ return {
1661
+ message: `Layout file updated: ${layoutResult.filePath}`,
1662
+ type: 'info'
1663
+ };
1664
+ }
1665
+ return {
1666
+ message: 'Layout file not updated.',
1667
+ type: 'error'
1668
+ };
1669
+ };
1670
+ const { message, type } = spinnerMessage();
1671
+ spinner.stop(logger_formatLogMessage(type, message));
1672
+ return {
1673
+ layoutUpdated: layoutResult.updated,
1674
+ layoutPath: layoutResult.filePath
1675
+ };
1676
+ }
1677
+ async function handleNextConfig(options) {
1678
+ const { projectRoot, backendURL, useEnvFile, spinner } = options;
1679
+ spinner.start('Updating Next.js config...');
1680
+ const configResult = await updateNextConfig({
1681
+ projectRoot,
1682
+ backendURL,
1683
+ useEnvFile
1684
+ });
1685
+ const spinnerMessage = ()=>{
1686
+ if (configResult.alreadyModified) return {
1687
+ message: 'Next.js config already has c15t rewrite rule. Skipped config update.',
1688
+ type: 'info'
1689
+ };
1690
+ if (configResult.updated && configResult.created) return {
1691
+ message: `Next.js config created: ${configResult.filePath}`,
1692
+ type: 'info'
1693
+ };
1694
+ if (configResult.updated) return {
1695
+ message: `Next.js config updated: ${configResult.filePath}`,
1696
+ type: 'info'
1697
+ };
1698
+ return {
1699
+ message: 'Next.js config not updated.',
1700
+ type: 'error'
1701
+ };
1702
+ };
1703
+ const { message, type } = spinnerMessage();
1704
+ spinner.stop(logger_formatLogMessage(type, message));
1705
+ return {
1706
+ nextConfigUpdated: configResult.updated,
1707
+ nextConfigPath: configResult.filePath,
1708
+ nextConfigCreated: configResult.created
1709
+ };
1710
+ }
1711
+ async function handleEnvFiles(options) {
1712
+ const { projectRoot, backendURL, pkg, spinner, cwd } = options;
1713
+ const envPath = node_path.join(projectRoot, '.env.local');
1714
+ const envExamplePath = node_path.join(projectRoot, '.env.example');
1715
+ spinner.start('Creating/updating environment files...');
1716
+ const envContent = generateEnvFileContent(backendURL, pkg);
1717
+ const envExampleContent = generateEnvExampleContent(pkg);
1718
+ const envVarName = getEnvVarName(pkg);
1719
+ try {
1720
+ const [envExists, envExampleExists] = await Promise.all([
1721
+ promises.access(envPath).then(()=>true).catch(()=>false),
1722
+ promises.access(envExamplePath).then(()=>true).catch(()=>false)
1723
+ ]);
1724
+ if (envExists) {
1725
+ const currentEnvContent = await promises.readFile(envPath, 'utf-8');
1726
+ if (!currentEnvContent.includes(envVarName)) await promises.appendFile(envPath, envContent);
1727
+ } else await promises.writeFile(envPath, envContent);
1728
+ if (envExampleExists) {
1729
+ const currentExampleContent = await promises.readFile(envExamplePath, 'utf-8');
1730
+ if (!currentExampleContent.includes(envVarName)) await promises.appendFile(envExamplePath, envExampleContent);
1731
+ } else await promises.writeFile(envExamplePath, envExampleContent);
1732
+ spinner.stop(logger_formatLogMessage('info', `Environment files added/updated successfully: ${picocolors.cyan(node_path.relative(cwd, envPath))} and ${picocolors.cyan(node_path.relative(cwd, envExamplePath))}`));
1733
+ } catch (error) {
1734
+ spinner.stop(logger_formatLogMessage('error', `Error processing environment files: ${error instanceof Error ? error.message : String(error)}`));
1735
+ throw error;
1736
+ }
1737
+ }
1738
+ async function generateFiles({ context, mode, spinner, useEnvFile, proxyNextjs, backendURL, enableSSR, enableDevTools, uiStyle, expandedTheme, selectedScripts }) {
1739
+ const result = {
1740
+ layoutUpdated: false
1741
+ };
1742
+ const { projectRoot, framework: { pkg } } = context;
1743
+ if ('@c15t/nextjs' === pkg || '@c15t/react' === pkg) {
1744
+ const layoutResult = await handleReactLayout({
1745
+ projectRoot,
1746
+ mode,
1747
+ backendURL,
1748
+ useEnvFile,
1749
+ proxyNextjs,
1750
+ enableSSR,
1751
+ enableDevTools,
1752
+ uiStyle,
1753
+ expandedTheme,
1754
+ selectedScripts,
1755
+ pkg,
1756
+ spinner,
1757
+ cwd: context.cwd
1758
+ });
1759
+ result.layoutUpdated = layoutResult.layoutUpdated;
1760
+ result.layoutPath = layoutResult.layoutPath;
1761
+ }
1762
+ if ('@c15t/nextjs' === pkg && proxyNextjs && ('hosted' === mode || 'c15t' === mode || 'self-hosted' === mode)) {
1763
+ const configResult = await handleNextConfig({
1764
+ projectRoot,
1765
+ backendURL,
1766
+ useEnvFile,
1767
+ spinner
1768
+ });
1769
+ result.nextConfigUpdated = configResult.nextConfigUpdated;
1770
+ result.nextConfigPath = configResult.nextConfigPath;
1771
+ result.nextConfigCreated = configResult.nextConfigCreated;
1772
+ }
1773
+ if ('c15t' === pkg) {
1774
+ spinner.start('Generating client configuration file...');
1775
+ result.configContent = generateClientConfigContent(mode, backendURL, useEnvFile, enableDevTools);
1776
+ result.configPath = node_path.join(projectRoot, 'c15t.config.ts');
1777
+ spinner.stop(logger_formatLogMessage('info', `Client configuration file generated: ${result.configContent}`));
1778
+ }
1779
+ if (useEnvFile && backendURL) await handleEnvFiles({
1780
+ projectRoot,
1781
+ backendURL,
1782
+ pkg,
1783
+ spinner,
1784
+ cwd: context.cwd
1785
+ });
1786
+ if (context.framework.tailwindVersion) {
1787
+ spinner.start('Checking Tailwind CSS compatibility...');
1788
+ const tailwindResult = await updateTailwindCss(projectRoot, context.framework.tailwindVersion);
1789
+ if (tailwindResult.updated) {
1790
+ result.tailwindCssUpdated = true;
1791
+ result.tailwindCssPath = tailwindResult.filePath;
1792
+ spinner.stop(logger_formatLogMessage('info', `Tailwind CSS updated for v3 compatibility: ${picocolors.cyan(node_path.relative(context.cwd, tailwindResult.filePath || ''))}`));
1793
+ } else spinner.stop(logger_formatLogMessage('debug', 'Tailwind CSS update not needed.'));
1794
+ }
1795
+ return result;
1796
+ }
1797
+ export { generateFiles };