@tantainnovative/create-ndpr 0.1.0 → 0.5.0
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.
- package/README.md +75 -56
- package/bin/index.mjs +369 -394
- package/package.json +14 -4
- package/templates/drizzle-client.ts +44 -0
- package/templates/drizzle-schema.ts +224 -117
- package/templates/env-example +6 -3
- package/templates/express-breach-route.ts +98 -0
- package/templates/express-consent-route.ts +336 -79
- package/templates/express-cross-border-route.ts +152 -0
- package/templates/express-dpia-route.ts +332 -0
- package/templates/express-dsr-route.ts +76 -0
- package/templates/express-lawful-basis-route.ts +146 -0
- package/templates/express-request-context.ts +130 -0
- package/templates/github-ndpr-audit.yml +31 -0
- package/templates/ndpr-audit.json +14 -0
- package/templates/nextjs-breach-route.ts +145 -79
- package/templates/nextjs-consent-route.ts +343 -70
- package/templates/nextjs-cross-border-route.ts +314 -0
- package/templates/nextjs-dpia-route.ts +510 -0
- package/templates/nextjs-dsr-route.ts +93 -58
- package/templates/nextjs-lawful-basis-route.ts +283 -0
- package/templates/nextjs-layout.tsx +122 -50
- package/templates/nextjs-request-context.ts +137 -0
- package/templates/prisma-schema.prisma +121 -51
- package/templates/express-setup.ts +0 -250
package/bin/index.mjs
CHANGED
|
@@ -1,503 +1,478 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import { createInterface } from 'readline';
|
|
3
|
+
import { createInterface } from 'node:readline';
|
|
4
4
|
import {
|
|
5
5
|
existsSync,
|
|
6
6
|
mkdirSync,
|
|
7
|
-
writeFileSync,
|
|
8
7
|
readFileSync,
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
} from 'fs';
|
|
12
|
-
import {
|
|
13
|
-
import { fileURLToPath } from 'url';
|
|
8
|
+
realpathSync,
|
|
9
|
+
writeFileSync,
|
|
10
|
+
} from 'node:fs';
|
|
11
|
+
import { dirname, join } from 'node:path';
|
|
12
|
+
import { fileURLToPath } from 'node:url';
|
|
14
13
|
|
|
15
14
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
16
15
|
const TEMPLATES_DIR = join(__dirname, '..', 'templates');
|
|
17
16
|
const CWD = process.cwd();
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
// ---------------------------------------------------------------------------
|
|
17
|
+
// Keep this exact pin synchronized with the repository root package version.
|
|
18
|
+
const TOOLKIT_VERSION = '6.0.0';
|
|
19
|
+
const FORCE = process.argv.slice(2).includes('--force');
|
|
22
20
|
|
|
23
21
|
const c = {
|
|
24
|
-
reset: '\x1b[0m',
|
|
25
|
-
|
|
26
|
-
dim: '\x1b[2m',
|
|
27
|
-
green: '\x1b[32m',
|
|
28
|
-
cyan: '\x1b[36m',
|
|
29
|
-
yellow: '\x1b[33m',
|
|
30
|
-
blue: '\x1b[34m',
|
|
31
|
-
magenta: '\x1b[35m',
|
|
32
|
-
red: '\x1b[31m',
|
|
33
|
-
white: '\x1b[37m',
|
|
22
|
+
reset: '\x1b[0m', bold: '\x1b[1m', dim: '\x1b[2m', green: '\x1b[32m',
|
|
23
|
+
cyan: '\x1b[36m', yellow: '\x1b[33m', red: '\x1b[31m',
|
|
34
24
|
};
|
|
35
|
-
|
|
36
|
-
const bold = (
|
|
37
|
-
const green = (
|
|
38
|
-
const cyan = (
|
|
39
|
-
const yellow = (
|
|
40
|
-
const dim = (
|
|
41
|
-
const red = (
|
|
42
|
-
|
|
43
|
-
// ---------------------------------------------------------------------------
|
|
44
|
-
// Banner
|
|
45
|
-
// ---------------------------------------------------------------------------
|
|
25
|
+
const colour = (code, value) => `${code}${value}${c.reset}`;
|
|
26
|
+
const bold = (value) => colour(c.bold, value);
|
|
27
|
+
const green = (value) => colour(c.green, value);
|
|
28
|
+
const cyan = (value) => colour(c.cyan, value);
|
|
29
|
+
const yellow = (value) => colour(c.yellow, value);
|
|
30
|
+
const dim = (value) => colour(c.dim, value);
|
|
31
|
+
const red = (value) => colour(c.red, value);
|
|
46
32
|
|
|
47
33
|
function printBanner() {
|
|
48
34
|
console.log();
|
|
49
|
-
console.log(bold(cyan('
|
|
50
|
-
console.log(
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
console.log(bold(cyan(' ╚═╝ ╚═══╝╚═════╝ ╚═╝ ╚═╝ ╚═╝')));
|
|
55
|
-
console.log();
|
|
56
|
-
console.log(bold(' create-ndpr') + dim(' — NDPA compliance scaffolder'));
|
|
57
|
-
console.log(dim(' Powered by @tantainnovative/ndpr-toolkit'));
|
|
35
|
+
console.log(bold(cyan(' create-ndpr')) + dim(' — NDPA compliance scaffolder'));
|
|
36
|
+
console.log(dim(` Generates @tantainnovative/ndpr-toolkit@${TOOLKIT_VERSION} integrations`));
|
|
37
|
+
if (FORCE) {
|
|
38
|
+
console.log(yellow(' --force enabled: existing generated files may be replaced.'));
|
|
39
|
+
}
|
|
58
40
|
console.log();
|
|
59
41
|
}
|
|
60
42
|
|
|
61
|
-
// ---------------------------------------------------------------------------
|
|
62
|
-
// Stack detection
|
|
63
|
-
// ---------------------------------------------------------------------------
|
|
64
|
-
|
|
65
43
|
function detectStack() {
|
|
66
|
-
const detected = {
|
|
67
|
-
framework: null, // 'nextjs-app' | 'nextjs-pages' | 'express' | null
|
|
68
|
-
orm: null, // 'prisma' | 'drizzle' | null
|
|
69
|
-
hasPackageJson: false,
|
|
70
|
-
};
|
|
71
|
-
|
|
72
|
-
// Check package.json exists
|
|
44
|
+
const detected = { framework: null, orm: null };
|
|
73
45
|
const pkgPath = join(CWD, 'package.json');
|
|
46
|
+
let dependencies = {};
|
|
74
47
|
if (existsSync(pkgPath)) {
|
|
75
|
-
detected.hasPackageJson = true;
|
|
76
48
|
try {
|
|
77
49
|
const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
if (deps['express']) {
|
|
84
|
-
detected.framework = 'express';
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
if (deps['drizzle-orm'] || deps['drizzle-kit']) {
|
|
88
|
-
detected.orm = 'drizzle';
|
|
89
|
-
}
|
|
90
|
-
if (deps['@prisma/client'] || deps['prisma']) {
|
|
91
|
-
detected.orm = 'prisma';
|
|
92
|
-
}
|
|
50
|
+
dependencies = { ...(pkg.dependencies ?? {}), ...(pkg.devDependencies ?? {}) };
|
|
51
|
+
if (dependencies.express) detected.framework = 'express';
|
|
52
|
+
if (dependencies['drizzle-orm'] || dependencies['drizzle-kit']) detected.orm = 'drizzle';
|
|
53
|
+
if (dependencies['@prisma/client'] || dependencies.prisma) detected.orm = 'prisma';
|
|
93
54
|
} catch {
|
|
94
|
-
|
|
55
|
+
console.log(yellow(' package.json could not be parsed; continuing with manual choices.'));
|
|
95
56
|
}
|
|
96
57
|
}
|
|
97
58
|
|
|
98
|
-
|
|
99
|
-
const
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
const hasAppDir =
|
|
104
|
-
existsSync(join(CWD, 'app')) ||
|
|
105
|
-
existsSync(join(CWD, 'src', 'app'));
|
|
106
|
-
detected.framework = hasAppDir ? 'nextjs-app' : 'nextjs-pages';
|
|
59
|
+
const nextConfigs = ['next.config.js', 'next.config.cjs', 'next.config.mjs', 'next.config.ts'];
|
|
60
|
+
const hasNext = Boolean(dependencies.next) || nextConfigs.some((file) => existsSync(join(CWD, file)));
|
|
61
|
+
if (hasNext) {
|
|
62
|
+
const hasApp = existsSync(join(CWD, 'app')) || existsSync(join(CWD, 'src', 'app'));
|
|
63
|
+
detected.framework = hasApp ? 'nextjs-app' : 'nextjs-pages';
|
|
107
64
|
}
|
|
108
65
|
|
|
109
|
-
// ORM detection via project structure (if not already found in deps)
|
|
110
66
|
if (!detected.orm) {
|
|
111
|
-
if (existsSync(join(CWD, 'prisma', 'schema.prisma')))
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
const drizzleConfigs = ['drizzle.config.ts', 'drizzle.config.js', 'drizzle.config.mjs'];
|
|
115
|
-
if (drizzleConfigs.some((f) => existsSync(join(CWD, f)))) {
|
|
116
|
-
detected.orm = 'drizzle';
|
|
117
|
-
}
|
|
67
|
+
if (existsSync(join(CWD, 'prisma', 'schema.prisma'))) detected.orm = 'prisma';
|
|
68
|
+
else if (['ts', 'js', 'mjs', 'cjs'].some((ext) => existsSync(join(CWD, `drizzle.config.${ext}`)))) {
|
|
69
|
+
detected.orm = 'drizzle';
|
|
118
70
|
}
|
|
119
71
|
}
|
|
120
|
-
|
|
121
72
|
return detected;
|
|
122
73
|
}
|
|
123
74
|
|
|
124
|
-
// ---------------------------------------------------------------------------
|
|
125
|
-
// Readline helpers
|
|
126
|
-
// ---------------------------------------------------------------------------
|
|
127
|
-
|
|
128
75
|
function createRL() {
|
|
129
|
-
return createInterface({
|
|
130
|
-
input: process.stdin,
|
|
131
|
-
output: process.stdout,
|
|
132
|
-
});
|
|
76
|
+
return createInterface({ input: process.stdin, output: process.stdout });
|
|
133
77
|
}
|
|
134
|
-
|
|
135
78
|
function ask(rl, question) {
|
|
136
|
-
return new Promise((resolve) =>
|
|
137
|
-
rl.question(question, (answer) => resolve(answer.trim()));
|
|
138
|
-
});
|
|
79
|
+
return new Promise((resolve) => rl.question(question, (answer) => resolve(answer.trim())));
|
|
139
80
|
}
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
console.log(red(` ${label} is required.`));
|
|
147
|
-
}
|
|
81
|
+
async function askValidated(rl, question, label, validate) {
|
|
82
|
+
while (true) {
|
|
83
|
+
const value = await ask(rl, question);
|
|
84
|
+
const problem = validate(value);
|
|
85
|
+
if (!problem) return value;
|
|
86
|
+
console.log(red(` ${label}: ${problem}`));
|
|
148
87
|
}
|
|
149
|
-
return value;
|
|
150
88
|
}
|
|
151
|
-
|
|
152
89
|
async function askChoice(rl, question, choices, defaultIndex = 0) {
|
|
153
|
-
const lines = choices.map((
|
|
154
|
-
const prompt = `${question}\n${lines}\n Choice [${defaultIndex + 1}]: `;
|
|
90
|
+
const lines = choices.map((choice, index) => ` ${index + 1}) ${choice}`).join('\n');
|
|
155
91
|
while (true) {
|
|
156
|
-
const raw = await ask(rl,
|
|
92
|
+
const raw = await ask(rl, `${question}\n${lines}\n Choice [${defaultIndex + 1}]: `);
|
|
157
93
|
if (!raw) return defaultIndex;
|
|
158
|
-
const
|
|
159
|
-
if (
|
|
160
|
-
console.log(yellow(`
|
|
94
|
+
const selected = Number.parseInt(raw, 10);
|
|
95
|
+
if (Number.isInteger(selected) && selected >= 1 && selected <= choices.length) return selected - 1;
|
|
96
|
+
console.log(yellow(` Enter a number from 1-${choices.length}.`));
|
|
161
97
|
}
|
|
162
98
|
}
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
const
|
|
166
|
-
const lines = options.map((o, i) => ` ${i + 1}) ${o}`).join('\n');
|
|
167
|
-
const prompt = `${question}${defaultStr}\n${lines}\n Enter numbers separated by commas (or press Enter for default): `;
|
|
168
|
-
|
|
99
|
+
async function askCheckboxes(rl, question, options, defaults) {
|
|
100
|
+
const lines = options.map((option, index) => ` ${index + 1}) ${option}`).join('\n');
|
|
101
|
+
const defaultText = defaults.map((index) => index + 1).join(',');
|
|
169
102
|
while (true) {
|
|
170
|
-
const raw = await ask(
|
|
103
|
+
const raw = await ask(
|
|
104
|
+
rl,
|
|
105
|
+
`${question} [default: ${defaultText}]\n${lines}\n Enter comma-separated numbers: `,
|
|
106
|
+
);
|
|
171
107
|
if (!raw) return defaults;
|
|
172
|
-
const
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
for (const p of parts) {
|
|
176
|
-
const n = parseInt(p, 10);
|
|
177
|
-
if (isNaN(n) || n < 1 || n > options.length) {
|
|
178
|
-
console.log(yellow(` Invalid option: ${p}. Enter numbers from 1-${options.length}.`));
|
|
179
|
-
valid = false;
|
|
180
|
-
break;
|
|
181
|
-
}
|
|
182
|
-
indices.push(n - 1);
|
|
108
|
+
const values = raw.split(',').map((value) => Number.parseInt(value.trim(), 10));
|
|
109
|
+
if (values.every((value) => Number.isInteger(value) && value >= 1 && value <= options.length)) {
|
|
110
|
+
return [...new Set(values.map((value) => value - 1))];
|
|
183
111
|
}
|
|
184
|
-
|
|
112
|
+
console.log(yellow(` Enter only numbers from 1-${options.length}.`));
|
|
185
113
|
}
|
|
186
114
|
}
|
|
187
|
-
|
|
188
115
|
async function askYesNo(rl, question, defaultYes = true) {
|
|
189
|
-
const
|
|
190
|
-
|
|
191
|
-
if (!answer) return defaultYes;
|
|
192
|
-
return answer.toLowerCase().startsWith('y');
|
|
116
|
+
const answer = await ask(rl, `${question} ${defaultYes ? '[Y/n]' : '[y/N]'}: `);
|
|
117
|
+
return answer ? answer.toLowerCase().startsWith('y') : defaultYes;
|
|
193
118
|
}
|
|
194
119
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
120
|
+
function javascriptStringLiteral(value) {
|
|
121
|
+
return JSON.stringify(String(value))
|
|
122
|
+
.replaceAll('\u2028', '\\u2028')
|
|
123
|
+
.replaceAll('\u2029', '\\u2029');
|
|
124
|
+
}
|
|
125
|
+
function javascriptTemplateLiteralContent(value) {
|
|
126
|
+
return JSON.stringify(String(value))
|
|
127
|
+
.slice(1, -1)
|
|
128
|
+
.replaceAll('`', '\\`')
|
|
129
|
+
.replaceAll('${', '\\${')
|
|
130
|
+
.replaceAll('\u2028', '\\u2028')
|
|
131
|
+
.replaceAll('\u2029', '\\u2029');
|
|
132
|
+
}
|
|
133
|
+
function commentTemplateValue(value) {
|
|
134
|
+
return String(value)
|
|
135
|
+
.replace(/[\u0000-\u001f\u007f\u2028\u2029]+/g, ' ')
|
|
136
|
+
.replaceAll('*/', '* /');
|
|
137
|
+
}
|
|
138
|
+
function tenantSlug(value) {
|
|
139
|
+
const slug = String(value).toLowerCase().normalize('NFKD')
|
|
140
|
+
.replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 60);
|
|
141
|
+
return slug || 'ndpr-tenant';
|
|
142
|
+
}
|
|
143
|
+
function createTemplateVars({ orgName, dpoEmail, framework, orm }) {
|
|
144
|
+
return {
|
|
145
|
+
ORG_NAME_COMMENT: commentTemplateValue(orgName),
|
|
146
|
+
ORG_NAME_LITERAL: javascriptStringLiteral(orgName),
|
|
147
|
+
DPO_EMAIL_LITERAL: javascriptStringLiteral(dpoEmail),
|
|
148
|
+
ORG_NAME_TEMPLATE: javascriptTemplateLiteralContent(orgName),
|
|
149
|
+
DPO_EMAIL_TEMPLATE: javascriptTemplateLiteralContent(dpoEmail),
|
|
150
|
+
TENANT_ID: tenantSlug(orgName),
|
|
151
|
+
TOOLKIT_VERSION,
|
|
152
|
+
FRAMEWORK: framework,
|
|
153
|
+
ORM: orm,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
198
156
|
|
|
199
157
|
function renderTemplate(templateName, vars) {
|
|
200
158
|
const templatePath = join(TEMPLATES_DIR, templateName);
|
|
201
|
-
if (!existsSync(templatePath)) {
|
|
202
|
-
throw new Error(`Template not found: ${templateName}`);
|
|
203
|
-
}
|
|
159
|
+
if (!existsSync(templatePath)) throw new Error(`Template not found: ${templateName}`);
|
|
204
160
|
let content = readFileSync(templatePath, 'utf8');
|
|
205
|
-
|
|
206
|
-
|
|
161
|
+
const ifPattern =
|
|
162
|
+
/(?:[ \t]*\/\/[ \t]*)?\{\{#if[ \t]+([A-Z_]+)=([a-zA-Z0-9_-]+)\}\}\s*([\s\S]*?)(?:[ \t]*\/\/[ \t]*)?\{\{\/if\}\}[ \t]*\n?/g;
|
|
163
|
+
content = content.replace(ifPattern, (_match, key, expected, body) =>
|
|
164
|
+
vars[key] === expected ? (body.endsWith('\n') ? body : `${body}\n`) : '',
|
|
165
|
+
);
|
|
166
|
+
const unresolvedDirective = content.match(/\{\{(?:#if|\/if)[^}]*\}\}/);
|
|
167
|
+
if (unresolvedDirective) {
|
|
168
|
+
throw new Error(`Unresolved template directive ${unresolvedDirective[0]} in ${templateName}`);
|
|
207
169
|
}
|
|
170
|
+
content = content.replace(/\{\{([A-Z_]+)\}\}/g, (placeholder, key) => {
|
|
171
|
+
if (!Object.hasOwn(vars, key)) {
|
|
172
|
+
throw new Error(`Unresolved template variable ${placeholder} in ${templateName}`);
|
|
173
|
+
}
|
|
174
|
+
return String(vars[key]);
|
|
175
|
+
});
|
|
208
176
|
return content;
|
|
209
177
|
}
|
|
210
178
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
179
|
+
const GENERATED_FILES = [];
|
|
180
|
+
const SKIPPED_FILES = [];
|
|
181
|
+
function skip(relativePath, reason) {
|
|
182
|
+
SKIPPED_FILES.push(relativePath);
|
|
183
|
+
console.log(` ${dim('-')} ${dim(relativePath)} ${dim(`(skipped — ${reason})`)}`);
|
|
184
|
+
}
|
|
185
|
+
function writeGenerated(relativePath, content, { neverOverwrite = false } = {}) {
|
|
186
|
+
const destination = join(CWD, relativePath);
|
|
187
|
+
const shouldOverwrite = FORCE && !neverOverwrite;
|
|
188
|
+
const directory = dirname(destination);
|
|
189
|
+
mkdirSync(directory, { recursive: true });
|
|
190
|
+
try {
|
|
191
|
+
writeFileSync(destination, content, {
|
|
192
|
+
encoding: 'utf8',
|
|
193
|
+
flag: shouldOverwrite ? 'w' : 'wx',
|
|
194
|
+
});
|
|
195
|
+
} catch (error) {
|
|
196
|
+
if (error && error.code === 'EEXIST') {
|
|
197
|
+
skip(
|
|
198
|
+
relativePath,
|
|
199
|
+
neverOverwrite
|
|
200
|
+
? 'existing file must be merged manually'
|
|
201
|
+
: 'already exists; pass --force to replace',
|
|
202
|
+
);
|
|
203
|
+
return false;
|
|
204
|
+
}
|
|
205
|
+
throw error;
|
|
215
206
|
}
|
|
216
|
-
|
|
207
|
+
GENERATED_FILES.push(relativePath);
|
|
208
|
+
console.log(` ${green('+')} ${relativePath}`);
|
|
209
|
+
return true;
|
|
217
210
|
}
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
// File generation
|
|
221
|
-
// ---------------------------------------------------------------------------
|
|
222
|
-
|
|
223
|
-
const GENERATED_FILES = [];
|
|
224
|
-
|
|
225
|
-
function generate(destRelative, templateName, vars = {}) {
|
|
226
|
-
const destPath = join(CWD, destRelative);
|
|
227
|
-
const content = renderTemplate(templateName, vars);
|
|
228
|
-
writeFile(destPath, content);
|
|
229
|
-
GENERATED_FILES.push(destRelative);
|
|
230
|
-
console.log(` ${green('+')} ${destRelative}`);
|
|
211
|
+
function generate(relativePath, templateName, vars, options) {
|
|
212
|
+
return writeGenerated(relativePath, renderTemplate(templateName, vars), options);
|
|
231
213
|
}
|
|
232
214
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
215
|
+
const PAGES_METHODS = {
|
|
216
|
+
consent: ['GET', 'POST', 'DELETE'],
|
|
217
|
+
dsr: ['GET', 'POST'],
|
|
218
|
+
breach: ['GET', 'POST'],
|
|
219
|
+
dpia: ['GET', 'POST', 'PUT', 'DELETE'],
|
|
220
|
+
'lawful-basis': ['GET', 'POST', 'PUT', 'DELETE'],
|
|
221
|
+
'cross-border': ['GET', 'POST', 'PUT', 'DELETE'],
|
|
222
|
+
};
|
|
223
|
+
function renderPagesRoute(templateName, vars, methods) {
|
|
224
|
+
let content = renderTemplate(templateName, vars)
|
|
225
|
+
.replace('Next.js App Router', 'Next.js Pages Router')
|
|
226
|
+
.replace(
|
|
227
|
+
"import { NextRequest, NextResponse } from 'next/server';",
|
|
228
|
+
"import { NextRequest, NextResponse } from 'next/server';\nimport type { NextApiRequest, NextApiResponse } from 'next';",
|
|
229
|
+
);
|
|
230
|
+
const map = methods.map((method) => `${method}`).join(', ');
|
|
231
|
+
content += `\n\ntype AppRouteHandler = (request: NextRequest) => Promise<Response>;\nconst pageHandlers: Record<string, AppRouteHandler> = { ${map} };\n\nexport default async function handler(req: NextApiRequest, res: NextApiResponse) {\n const method = (req.method ?? '').toUpperCase();\n const routeHandler = pageHandlers[method];\n if (!routeHandler) {\n res.setHeader('Allow', Object.keys(pageHandlers));\n return res.status(405).json({ error: 'Method not allowed' });\n }\n\n const headers = new Headers();\n for (const [name, value] of Object.entries(req.headers)) {\n if (Array.isArray(value)) value.forEach((item) => headers.append(name, item));\n else if (value !== undefined) headers.set(name, value);\n }\n headers.delete('content-length');\n const init: Omit<RequestInit, 'signal'> = { method, headers };\n if (method !== 'GET' && method !== 'HEAD') {\n init.body = typeof req.body === 'string' ? req.body : JSON.stringify(req.body ?? {});\n if (!headers.has('content-type')) headers.set('content-type', 'application/json');\n }\n\n const request = new NextRequest(new URL(req.url ?? '/', 'http://localhost'), init);\n const response = await routeHandler(request);\n response.headers.forEach((value, name) => res.setHeader(name, value));\n const responseBody = await response.text();\n if (!responseBody) return res.status(response.status).end();\n return res.status(response.status).send(responseBody);\n}\n`;
|
|
232
|
+
return content;
|
|
238
233
|
}
|
|
239
234
|
|
|
240
|
-
function
|
|
241
|
-
|
|
235
|
+
function renderExpressIndex(selectedModules) {
|
|
236
|
+
const routeNames = {
|
|
237
|
+
consent: 'consentRouter', dsr: 'dsrRouter', breach: 'breachRouter',
|
|
238
|
+
dpia: 'dpiaRouter', 'lawful-basis': 'lawfulBasisRouter',
|
|
239
|
+
'cross-border': 'crossBorderRouter',
|
|
240
|
+
};
|
|
241
|
+
const enabled = selectedModules.filter((module) => routeNames[module]);
|
|
242
|
+
const imports = enabled.map((module) =>
|
|
243
|
+
`import { ${routeNames[module]} } from './routes/${module}';`,
|
|
244
|
+
).join('\n');
|
|
245
|
+
const mounts = enabled.map((module) =>
|
|
246
|
+
` router.use('/${module}', ${routeNames[module]});`,
|
|
247
|
+
).join('\n');
|
|
248
|
+
return `import { Router } from 'express';\n${imports}\n\nexport function createNDPRRouter(): Router {\n const router = Router();\n${mounts}\n return router;\n}\n`;
|
|
242
249
|
}
|
|
243
250
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
251
|
+
const NEXT_TEMPLATES = {
|
|
252
|
+
consent: 'nextjs-consent-route.ts', dsr: 'nextjs-dsr-route.ts',
|
|
253
|
+
breach: 'nextjs-breach-route.ts', dpia: 'nextjs-dpia-route.ts',
|
|
254
|
+
'lawful-basis': 'nextjs-lawful-basis-route.ts',
|
|
255
|
+
'cross-border': 'nextjs-cross-border-route.ts',
|
|
256
|
+
};
|
|
257
|
+
const EXPRESS_TEMPLATES = {
|
|
258
|
+
consent: 'express-consent-route.ts', dsr: 'express-dsr-route.ts',
|
|
259
|
+
breach: 'express-breach-route.ts', dpia: 'express-dpia-route.ts',
|
|
260
|
+
'lawful-basis': 'express-lawful-basis-route.ts',
|
|
261
|
+
'cross-border': 'express-cross-border-route.ts',
|
|
262
|
+
};
|
|
247
263
|
|
|
248
|
-
|
|
249
|
-
|
|
264
|
+
/**
|
|
265
|
+
* Render the exact integration files produced by the interactive CLI without
|
|
266
|
+
* reading prompts or writing to disk. The verifier consumes this same API.
|
|
267
|
+
*/
|
|
268
|
+
function renderIntegrationFiles({
|
|
269
|
+
framework,
|
|
270
|
+
orm,
|
|
271
|
+
selectedModules,
|
|
272
|
+
vars,
|
|
273
|
+
useSrcDirectory = false,
|
|
274
|
+
}) {
|
|
275
|
+
if (!['nextjs-app', 'nextjs-pages', 'express', 'none'].includes(framework)) {
|
|
276
|
+
throw new Error(`Unsupported framework: ${framework}`);
|
|
277
|
+
}
|
|
278
|
+
if (!['prisma', 'drizzle', 'none'].includes(orm)) {
|
|
279
|
+
throw new Error(`Unsupported ORM: ${orm}`);
|
|
280
|
+
}
|
|
250
281
|
|
|
251
|
-
|
|
252
|
-
const
|
|
282
|
+
const templateVars = { ...vars, FRAMEWORK: framework, ORM: orm };
|
|
283
|
+
const modules = [...new Set(selectedModules)];
|
|
284
|
+
const files = [];
|
|
285
|
+
const addTemplate = (relativePath, templateName, routeVars = templateVars, options) => {
|
|
286
|
+
files.push({
|
|
287
|
+
relativePath,
|
|
288
|
+
content: renderTemplate(templateName, routeVars),
|
|
289
|
+
...(options ? { options } : {}),
|
|
290
|
+
});
|
|
291
|
+
};
|
|
253
292
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
'
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
293
|
+
if (orm === 'prisma') {
|
|
294
|
+
addTemplate(
|
|
295
|
+
'prisma/schema.prisma',
|
|
296
|
+
'prisma-schema.prisma',
|
|
297
|
+
templateVars,
|
|
298
|
+
{ neverOverwrite: true },
|
|
299
|
+
);
|
|
300
|
+
} else if (orm === 'drizzle') {
|
|
301
|
+
addTemplate('src/drizzle/ndpr-schema.ts', 'drizzle-schema.ts');
|
|
302
|
+
addTemplate('src/drizzle/index.ts', 'drizzle-client.ts');
|
|
264
303
|
}
|
|
265
304
|
|
|
266
|
-
if (
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
305
|
+
if (framework === 'nextjs-app' || framework === 'nextjs-pages') {
|
|
306
|
+
const sourceRoot = useSrcDirectory ? 'src/' : '';
|
|
307
|
+
const appDir = `${sourceRoot}app`;
|
|
308
|
+
const pagesDir = `${sourceRoot}pages`;
|
|
309
|
+
const contextPath = `${sourceRoot}ndpr/request-context.ts`;
|
|
310
|
+
const drizzleBase = framework === 'nextjs-app'
|
|
311
|
+
? (useSrcDirectory ? '../../../drizzle' : '../../../src/drizzle')
|
|
312
|
+
: (useSrcDirectory ? '../../drizzle' : '../../src/drizzle');
|
|
313
|
+
addTemplate(contextPath, 'nextjs-request-context.ts');
|
|
314
|
+
|
|
315
|
+
if (modules.includes('consent')) {
|
|
316
|
+
addTemplate(
|
|
317
|
+
framework === 'nextjs-app'
|
|
318
|
+
? `${appDir}/ndpr-layout.tsx`
|
|
319
|
+
: `${pagesDir}/ndpr-provider.tsx`,
|
|
320
|
+
'nextjs-layout.tsx',
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
for (const [moduleName, template] of Object.entries(NEXT_TEMPLATES)) {
|
|
325
|
+
if (!modules.includes(moduleName)) continue;
|
|
326
|
+
const routeVars = {
|
|
327
|
+
...templateVars,
|
|
328
|
+
NDPR_CONTEXT_IMPORT: framework === 'nextjs-app'
|
|
329
|
+
? '../../../ndpr/request-context'
|
|
330
|
+
: '../../ndpr/request-context',
|
|
331
|
+
NDPR_DB_IMPORT: drizzleBase,
|
|
332
|
+
NDPR_SCHEMA_IMPORT: `${drizzleBase}/ndpr-schema`,
|
|
333
|
+
};
|
|
334
|
+
if (framework === 'nextjs-app') {
|
|
335
|
+
addTemplate(`${appDir}/api/${moduleName}/route.ts`, template, routeVars);
|
|
336
|
+
} else {
|
|
337
|
+
files.push({
|
|
338
|
+
relativePath: `${pagesDir}/api/${moduleName}.ts`,
|
|
339
|
+
content: renderPagesRoute(template, routeVars, PAGES_METHODS[moduleName]),
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
} else if (framework === 'express') {
|
|
344
|
+
addTemplate('src/ndpr/request-context.ts', 'express-request-context.ts');
|
|
345
|
+
for (const [moduleName, template] of Object.entries(EXPRESS_TEMPLATES)) {
|
|
346
|
+
if (!modules.includes(moduleName)) continue;
|
|
347
|
+
addTemplate(`src/ndpr/routes/${moduleName}.ts`, template, {
|
|
348
|
+
...templateVars,
|
|
349
|
+
NDPR_CONTEXT_IMPORT: '../request-context',
|
|
350
|
+
NDPR_DB_IMPORT: '../../drizzle',
|
|
351
|
+
NDPR_SCHEMA_IMPORT: '../../drizzle/ndpr-schema',
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
files.push({
|
|
355
|
+
relativePath: 'src/ndpr/index.ts',
|
|
356
|
+
content: renderExpressIndex(modules),
|
|
357
|
+
});
|
|
270
358
|
}
|
|
359
|
+
|
|
360
|
+
return files;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
async function main() {
|
|
364
|
+
printBanner();
|
|
365
|
+
const detected = detectStack();
|
|
366
|
+
console.log(bold(' Detected project setup:'));
|
|
367
|
+
console.log(` ${cyan('Framework:')} ${detected.framework ?? dim('not detected')}`);
|
|
368
|
+
console.log(` ${cyan('ORM:')} ${detected.orm ?? dim('not detected')}`);
|
|
271
369
|
console.log();
|
|
272
370
|
|
|
273
371
|
const rl = createRL();
|
|
274
|
-
|
|
275
372
|
try {
|
|
276
|
-
|
|
277
|
-
console.log(bold(' Organisation details'));
|
|
278
|
-
console.log(dim(' These are embedded in the generated files.'));
|
|
279
|
-
console.log();
|
|
280
|
-
|
|
281
|
-
const orgName = await askRequired(
|
|
373
|
+
const orgName = await askValidated(
|
|
282
374
|
rl,
|
|
283
|
-
` ${cyan('Organisation name')}
|
|
375
|
+
` ${cyan('Organisation name')}: `,
|
|
284
376
|
'Organisation name',
|
|
377
|
+
(value) => !value ? 'is required' : value.length > 120 ? 'must be at most 120 characters' : /[\u0000-\u001f]/.test(value) ? 'contains control characters' : null,
|
|
285
378
|
);
|
|
286
|
-
|
|
287
|
-
const dpoEmail = await askRequired(
|
|
379
|
+
const dpoEmail = await askValidated(
|
|
288
380
|
rl,
|
|
289
|
-
` ${cyan('DPO email address')}
|
|
381
|
+
` ${cyan('DPO email address')}: `,
|
|
290
382
|
'DPO email',
|
|
383
|
+
(value) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value) ? null : 'must be a valid email address',
|
|
291
384
|
);
|
|
292
|
-
console.log();
|
|
293
|
-
|
|
294
|
-
// --- Framework confirmation/override ---
|
|
295
|
-
const frameworkLabels = [
|
|
296
|
-
'Next.js — App Router',
|
|
297
|
-
'Next.js — Pages Router',
|
|
298
|
-
'Express',
|
|
299
|
-
'None (generate shared files only)',
|
|
300
|
-
];
|
|
301
|
-
const frameworkValues = ['nextjs-app', 'nextjs-pages', 'express', 'none'];
|
|
302
385
|
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
const
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
const framework = frameworkValues[frameworkIdx];
|
|
314
|
-
console.log();
|
|
315
|
-
|
|
316
|
-
// --- Modules ---
|
|
317
|
-
const moduleOptions = [
|
|
318
|
-
'consent — NDPA §25-26 consent management',
|
|
319
|
-
'dsr — Data Subject Rights requests (§34-38)',
|
|
320
|
-
'breach — Breach notification workflow (§40)',
|
|
321
|
-
'policy — Privacy policy generation',
|
|
322
|
-
'dpia — Data Protection Impact Assessment',
|
|
323
|
-
'lawful-basis — Lawful basis register',
|
|
324
|
-
'cross-border — Cross-border transfer management',
|
|
325
|
-
'ropa — Record of Processing Activities',
|
|
386
|
+
const frameworkLabels = ['Next.js — App Router', 'Next.js — Pages Router', 'Express', 'None'];
|
|
387
|
+
const frameworkValues = ['nextjs-app', 'nextjs-pages', 'express', 'none'];
|
|
388
|
+
const frameworkDefault = Math.max(0, frameworkValues.indexOf(detected.framework ?? 'none'));
|
|
389
|
+
const frameworkIndex = await askChoice(rl, ` ${cyan('Framework')}`, frameworkLabels, frameworkDefault);
|
|
390
|
+
const framework = frameworkValues[frameworkIndex];
|
|
391
|
+
|
|
392
|
+
const moduleLabels = [
|
|
393
|
+
'consent — NDPA §25-26', 'dsr — data-subject rights', 'breach — notification',
|
|
394
|
+
'policy — privacy policy', 'dpia — impact assessment',
|
|
395
|
+
'lawful-basis — processing register', 'cross-border — transfer register', 'ropa — processing activities',
|
|
326
396
|
];
|
|
327
397
|
const moduleValues = ['consent', 'dsr', 'breach', 'policy', 'dpia', 'lawful-basis', 'cross-border', 'ropa'];
|
|
398
|
+
const moduleIndices = await askCheckboxes(rl, ` ${cyan('Modules')}`, moduleLabels, [0, 1, 2]);
|
|
399
|
+
const selectedModules = moduleIndices.map((index) => moduleValues[index]);
|
|
328
400
|
|
|
329
|
-
|
|
330
|
-
const selectedModuleIndices = await askCheckboxes(
|
|
331
|
-
rl,
|
|
332
|
-
` ${cyan('Which modules do you want to include?')}`,
|
|
333
|
-
moduleOptions,
|
|
334
|
-
[0, 1, 2], // consent, dsr, breach by default
|
|
335
|
-
);
|
|
336
|
-
const selectedModules = selectedModuleIndices.map((i) => moduleValues[i]);
|
|
337
|
-
console.log();
|
|
338
|
-
|
|
339
|
-
// --- ORM ---
|
|
340
|
-
const ormLabels = ['Prisma', 'Drizzle', 'None (skip database schema)'];
|
|
401
|
+
const ormLabels = ['Prisma', 'Drizzle', 'None (development stores only)'];
|
|
341
402
|
const ormValues = ['prisma', 'drizzle', 'none'];
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
console.log(
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
detectedOrmIdx,
|
|
352
|
-
);
|
|
353
|
-
const orm = ormValues[ormIdx];
|
|
354
|
-
console.log();
|
|
355
|
-
|
|
356
|
-
// --- Confirm ---
|
|
357
|
-
console.log(bold(' Summary'));
|
|
358
|
-
console.log(` ${cyan('Organisation:')} ${orgName}`);
|
|
359
|
-
console.log(` ${cyan('DPO email:')} ${dpoEmail}`);
|
|
360
|
-
console.log(` ${cyan('Framework:')} ${frameworkLabels[frameworkIdx]}`);
|
|
361
|
-
console.log(` ${cyan('ORM:')} ${ormLabels[ormIdx]}`);
|
|
362
|
-
console.log(` ${cyan('Modules:')} ${selectedModules.join(', ')}`);
|
|
363
|
-
console.log();
|
|
364
|
-
|
|
365
|
-
const confirmed = await askYesNo(rl, ` ${cyan('Generate files?')}`, true);
|
|
366
|
-
if (!confirmed) {
|
|
403
|
+
const ormDefault = Math.max(0, ormValues.indexOf(detected.orm ?? 'none'));
|
|
404
|
+
const ormIndex = await askChoice(rl, ` ${cyan('ORM')}`, ormLabels, ormDefault);
|
|
405
|
+
const orm = ormValues[ormIndex];
|
|
406
|
+
|
|
407
|
+
console.log(`\n ${cyan('Organisation:')} ${orgName}`);
|
|
408
|
+
console.log(` ${cyan('Framework:')} ${frameworkLabels[frameworkIndex]}`);
|
|
409
|
+
console.log(` ${cyan('ORM:')} ${ormLabels[ormIndex]}`);
|
|
410
|
+
console.log(` ${cyan('Modules:')} ${selectedModules.join(', ') || 'none'}\n`);
|
|
411
|
+
if (!await askYesNo(rl, ` ${cyan('Generate files?')}`)) {
|
|
367
412
|
console.log(yellow('\n Aborted. No files were written.\n'));
|
|
368
|
-
|
|
413
|
+
return;
|
|
369
414
|
}
|
|
370
|
-
console.log();
|
|
371
|
-
|
|
372
|
-
// -------------------------------------------------------------------------
|
|
373
|
-
// File generation
|
|
374
|
-
// -------------------------------------------------------------------------
|
|
375
|
-
|
|
376
|
-
console.log(bold(' Generating files...'));
|
|
377
|
-
console.log();
|
|
378
415
|
|
|
379
|
-
const vars = {
|
|
380
|
-
|
|
381
|
-
DPO_EMAIL: dpoEmail,
|
|
382
|
-
};
|
|
383
|
-
|
|
384
|
-
// .env.example
|
|
416
|
+
const vars = createTemplateVars({ orgName, dpoEmail, framework, orm });
|
|
417
|
+
console.log(`\n${bold(' Generating files...')}\n`);
|
|
385
418
|
generate('.env.example', 'env-example', vars);
|
|
386
419
|
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
// Framework-specific files
|
|
404
|
-
if (framework === 'nextjs-app') {
|
|
405
|
-
const appDir = existsSync(join(CWD, 'src', 'app')) ? 'src/app' : 'app';
|
|
406
|
-
|
|
407
|
-
// Layout wrapper
|
|
408
|
-
generate(`${appDir}/ndpr-layout.tsx`, 'nextjs-layout.tsx', vars);
|
|
409
|
-
|
|
410
|
-
// API routes per selected module
|
|
411
|
-
if (selectedModules.includes('consent')) {
|
|
412
|
-
generate(`${appDir}/api/consent/route.ts`, 'nextjs-consent-route.ts', vars);
|
|
413
|
-
}
|
|
414
|
-
if (selectedModules.includes('dsr')) {
|
|
415
|
-
generate(`${appDir}/api/dsr/route.ts`, 'nextjs-dsr-route.ts', vars);
|
|
416
|
-
}
|
|
417
|
-
if (selectedModules.includes('breach')) {
|
|
418
|
-
generate(`${appDir}/api/breach/route.ts`, 'nextjs-breach-route.ts', vars);
|
|
419
|
-
}
|
|
420
|
-
} else if (framework === 'nextjs-pages') {
|
|
421
|
-
console.log(yellow(' Note: Pages Router API routes generated under pages/api/'));
|
|
422
|
-
if (selectedModules.includes('consent')) {
|
|
423
|
-
generate('pages/api/consent.ts', 'nextjs-consent-route.ts', vars);
|
|
424
|
-
}
|
|
425
|
-
if (selectedModules.includes('dsr')) {
|
|
426
|
-
generate('pages/api/dsr.ts', 'nextjs-dsr-route.ts', vars);
|
|
427
|
-
}
|
|
428
|
-
if (selectedModules.includes('breach')) {
|
|
429
|
-
generate('pages/api/breach.ts', 'nextjs-breach-route.ts', vars);
|
|
430
|
-
}
|
|
431
|
-
} else if (framework === 'express') {
|
|
432
|
-
generate('src/ndpr/index.ts', 'express-setup.ts', vars);
|
|
433
|
-
if (selectedModules.includes('consent')) {
|
|
434
|
-
generate('src/ndpr/routes/consent.ts', 'express-consent-route.ts', vars);
|
|
435
|
-
}
|
|
420
|
+
const useSrcDirectory = framework === 'nextjs-app'
|
|
421
|
+
? existsSync(join(CWD, 'src', 'app'))
|
|
422
|
+
: framework === 'nextjs-pages'
|
|
423
|
+
? existsSync(join(CWD, 'src', 'pages'))
|
|
424
|
+
: false;
|
|
425
|
+
const integrationFiles = renderIntegrationFiles({
|
|
426
|
+
framework,
|
|
427
|
+
orm,
|
|
428
|
+
selectedModules,
|
|
429
|
+
vars,
|
|
430
|
+
useSrcDirectory,
|
|
431
|
+
});
|
|
432
|
+
for (const file of integrationFiles) {
|
|
433
|
+
writeGenerated(file.relativePath, file.content, file.options);
|
|
436
434
|
}
|
|
437
435
|
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
// -------------------------------------------------------------------------
|
|
441
|
-
|
|
442
|
-
console.log();
|
|
443
|
-
console.log(bold(green(' Done! Files generated:')));
|
|
444
|
-
for (const f of GENERATED_FILES) {
|
|
445
|
-
console.log(` ${green('✓')} ${f}`);
|
|
446
|
-
}
|
|
447
|
-
console.log();
|
|
436
|
+
generate('ndpr.audit.json', 'ndpr-audit.json', vars);
|
|
437
|
+
generate('.github/workflows/ndpr-audit.yml', 'github-ndpr-audit.yml', vars);
|
|
448
438
|
|
|
449
|
-
|
|
439
|
+
console.log(`\n${bold(green(' Done.'))} Generated ${GENERATED_FILES.length} file(s); skipped ${SKIPPED_FILES.length}.\n`);
|
|
450
440
|
console.log(bold(' Next steps:'));
|
|
451
|
-
console.log();
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
console.log();
|
|
457
|
-
console.log(
|
|
458
|
-
console.log(dim(' pnpm add @prisma/client'));
|
|
459
|
-
console.log(dim(' pnpm add -D prisma'));
|
|
460
|
-
console.log(dim(' pnpm prisma migrate dev --name ndpr-init'));
|
|
461
|
-
console.log();
|
|
462
|
-
} else if (orm === 'drizzle') {
|
|
463
|
-
console.log(` ${cyan('1.')} Set your database URL in .env:`);
|
|
464
|
-
console.log(dim(' DATABASE_URL="postgresql://user:password@localhost:5432/mydb_dev"'));
|
|
465
|
-
console.log();
|
|
466
|
-
console.log(` ${cyan('2.')} Install Drizzle and push the schema:`);
|
|
467
|
-
console.log(dim(' pnpm add drizzle-orm @paralleldrive/cuid2'));
|
|
468
|
-
console.log(dim(' pnpm add -D drizzle-kit'));
|
|
469
|
-
console.log(dim(' pnpm drizzle-kit push'));
|
|
470
|
-
console.log();
|
|
441
|
+
console.log(dim(` 1. Install the exact toolkit version: pnpm add @tantainnovative/ndpr-toolkit@${TOOLKIT_VERSION}`));
|
|
442
|
+
console.log(dim(' 2. Set DATABASE_URL and NDPR_TENANT_ID in your server environment.'));
|
|
443
|
+
console.log(dim(' 3. Connect the generated request-context resolver to verified authentication.'));
|
|
444
|
+
if (orm === 'prisma') console.log(dim(' 4. Run: pnpm prisma migrate dev --name ndpr-init'));
|
|
445
|
+
if (orm === 'drizzle') {
|
|
446
|
+
console.log(dim(' 4. Pass your Drizzle instance to configureNDPRDatabase() during server startup.'));
|
|
447
|
+
console.log(dim(' Then run your pinned-compatible drizzle-kit migration command.'));
|
|
471
448
|
}
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
if (framework === 'nextjs-app' || framework === 'nextjs-pages') {
|
|
478
|
-
const appDir = existsSync(join(CWD, 'src', 'app')) ? 'src/app' : 'app';
|
|
479
|
-
console.log(` ${cyan(`${orm === 'none' ? '2' : '4'}`.padStart(1))}. Wrap your root layout with NDPRLayout:`);
|
|
480
|
-
console.log(dim(` // ${appDir}/layout.tsx`));
|
|
481
|
-
console.log(dim(` import NDPRLayout from './${appDir.includes('src') ? '' : ''}ndpr-layout';`));
|
|
482
|
-
console.log(dim(' // Render <NDPRLayout>{children}</NDPRLayout> inside your <body>'));
|
|
483
|
-
console.log();
|
|
449
|
+
if (framework === 'nextjs-pages' && selectedModules.includes('consent')) {
|
|
450
|
+
console.log(dim(' 5. Wrap Component with NDPRClientProvider from pages/ndpr-provider in pages/_app.tsx.'));
|
|
451
|
+
} else if (framework === 'nextjs-app' && selectedModules.includes('consent')) {
|
|
452
|
+
console.log(dim(' 5. Render NDPRClientProvider from app/ndpr-layout.tsx inside app/layout.tsx.'));
|
|
484
453
|
} else if (framework === 'express') {
|
|
485
|
-
console.log(
|
|
486
|
-
console.log(dim(" import { createNDPRRouter } from './src/ndpr';"));
|
|
487
|
-
console.log(dim(" app.use('/api/ndpr', createNDPRRouter());"));
|
|
488
|
-
console.log();
|
|
454
|
+
console.log(dim(" 5. Mount createNDPRRouter() at your chosen '/api/ndpr' path."));
|
|
489
455
|
}
|
|
490
|
-
|
|
491
|
-
console.log(dim(' Documentation: https://ndpr-toolkit.tantainnovative.com'));
|
|
492
|
-
console.log(dim(' Repository: https://github.com/tantainnovative/ndpr-toolkit'));
|
|
493
|
-
console.log();
|
|
494
|
-
|
|
456
|
+
console.log(dim(' Review every generated TODO and do not deploy in-memory stores as evidence.\n'));
|
|
495
457
|
} finally {
|
|
496
458
|
rl.close();
|
|
497
459
|
}
|
|
498
460
|
}
|
|
499
461
|
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
462
|
+
export {
|
|
463
|
+
createTemplateVars,
|
|
464
|
+
renderIntegrationFiles,
|
|
465
|
+
renderPagesRoute,
|
|
466
|
+
renderTemplate,
|
|
467
|
+
};
|
|
468
|
+
|
|
469
|
+
const isDirectExecution = Boolean(
|
|
470
|
+
process.argv[1]
|
|
471
|
+
&& realpathSync(process.argv[1]) === realpathSync(fileURLToPath(import.meta.url)),
|
|
472
|
+
);
|
|
473
|
+
if (isDirectExecution) {
|
|
474
|
+
main().catch((error) => {
|
|
475
|
+
console.error(red(`\n Error: ${error instanceof Error ? error.message : String(error)}\n`));
|
|
476
|
+
process.exitCode = 1;
|
|
477
|
+
});
|
|
478
|
+
}
|