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

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