open-brandkit 0.4.7
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/LICENSE +72 -0
- package/README.md +284 -0
- package/dist/adapters/next/brandkit-page.d.ts +15 -0
- package/dist/adapters/next/brandkit-page.d.ts.map +1 -0
- package/dist/adapters/next/brandkit-page.js +1085 -0
- package/dist/adapters/next/brandkit-page.js.map +1 -0
- package/dist/adapters/next/index.d.ts +3 -0
- package/dist/adapters/next/index.d.ts.map +1 -0
- package/dist/adapters/next/index.js +3 -0
- package/dist/adapters/next/index.js.map +1 -0
- package/dist/adapters/next/manifest.d.ts +33 -0
- package/dist/adapters/next/manifest.d.ts.map +1 -0
- package/dist/adapters/next/manifest.js +57 -0
- package/dist/adapters/next/manifest.js.map +1 -0
- package/dist/adapters/next/route-handlers.d.ts +102 -0
- package/dist/adapters/next/route-handlers.d.ts.map +1 -0
- package/dist/adapters/next/route-handlers.js +451 -0
- package/dist/adapters/next/route-handlers.js.map +1 -0
- package/dist/adapters/next/server.d.ts +2 -0
- package/dist/adapters/next/server.d.ts.map +1 -0
- package/dist/adapters/next/server.js +2 -0
- package/dist/adapters/next/server.js.map +1 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +1079 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/core/assets.d.ts +34 -0
- package/dist/core/assets.d.ts.map +1 -0
- package/dist/core/assets.js +200 -0
- package/dist/core/assets.js.map +1 -0
- package/dist/core/banner-renderer.d.ts +21 -0
- package/dist/core/banner-renderer.d.ts.map +1 -0
- package/dist/core/banner-renderer.js +119 -0
- package/dist/core/banner-renderer.js.map +1 -0
- package/dist/core/build.d.ts +13 -0
- package/dist/core/build.d.ts.map +1 -0
- package/dist/core/build.js +345 -0
- package/dist/core/build.js.map +1 -0
- package/dist/core/colors.d.ts +12 -0
- package/dist/core/colors.d.ts.map +1 -0
- package/dist/core/colors.js +335 -0
- package/dist/core/colors.js.map +1 -0
- package/dist/core/config.d.ts +103 -0
- package/dist/core/config.d.ts.map +1 -0
- package/dist/core/config.js +119 -0
- package/dist/core/config.js.map +1 -0
- package/dist/core/index.d.ts +8 -0
- package/dist/core/index.d.ts.map +1 -0
- package/dist/core/index.js +8 -0
- package/dist/core/index.js.map +1 -0
- package/dist/core/static-page.d.ts +3 -0
- package/dist/core/static-page.d.ts.map +1 -0
- package/dist/core/static-page.js +1830 -0
- package/dist/core/static-page.js.map +1 -0
- package/dist/core/types.d.ts +163 -0
- package/dist/core/types.d.ts.map +1 -0
- package/dist/core/types.js +2 -0
- package/dist/core/types.js.map +1 -0
- package/docs/STYLE_CONTRACT.md +48 -0
- package/examples/acme-studio-color-system.md +99 -0
- package/package.json +89 -0
|
@@ -0,0 +1,1079 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { spawn } from 'node:child_process';
|
|
3
|
+
import { access, mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { createInterface } from 'node:readline/promises';
|
|
6
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
7
|
+
import { createJiti } from 'jiti';
|
|
8
|
+
import { readBrandAssetFiles } from '../core/assets.js';
|
|
9
|
+
import { buildBrandKit } from '../core/build.js';
|
|
10
|
+
import { loadBrandKitColors } from '../core/colors.js';
|
|
11
|
+
const defaultConfigNames = [
|
|
12
|
+
'brandkit.config.ts',
|
|
13
|
+
'brandkit.config.mts',
|
|
14
|
+
'brandkit.config.js',
|
|
15
|
+
'brandkit.config.mjs',
|
|
16
|
+
];
|
|
17
|
+
function printHelp() {
|
|
18
|
+
console.log(`Open BrandKit
|
|
19
|
+
|
|
20
|
+
Usage:
|
|
21
|
+
open-brandkit init [options]
|
|
22
|
+
open-brandkit build [--config brandkit.config.ts]
|
|
23
|
+
|
|
24
|
+
Commands:
|
|
25
|
+
init Run the Next.js App Router installer wizard and create the Brand Kit.
|
|
26
|
+
build Generate public/brandkit assets, manifest, downloads, and page.
|
|
27
|
+
|
|
28
|
+
Init options:
|
|
29
|
+
--yes Use detected/default answers.
|
|
30
|
+
--force Overwrite generated files if they already exist.
|
|
31
|
+
--build Run the Brand Kit build after installing.
|
|
32
|
+
--install Run the package-manager install after writing package.json.
|
|
33
|
+
--brand "Name" Brand name.
|
|
34
|
+
--short-name "Name" Short brand name.
|
|
35
|
+
--logos path Logo source directory.
|
|
36
|
+
--colors path Color source file.
|
|
37
|
+
--route /brandkit Website route.
|
|
38
|
+
--app-dir src/app Next.js app directory.
|
|
39
|
+
`);
|
|
40
|
+
}
|
|
41
|
+
function hasFlag(args, flag) {
|
|
42
|
+
return args.includes(flag);
|
|
43
|
+
}
|
|
44
|
+
function hasOption(args, option) {
|
|
45
|
+
return args.some((arg) => arg === option || arg.startsWith(`${option}=`));
|
|
46
|
+
}
|
|
47
|
+
function getFlagValue(args, flag) {
|
|
48
|
+
const index = args.indexOf(flag);
|
|
49
|
+
if (index === -1)
|
|
50
|
+
return null;
|
|
51
|
+
return args[index + 1] ?? null;
|
|
52
|
+
}
|
|
53
|
+
function toPosixPath(value) {
|
|
54
|
+
return value.split(path.sep).join(path.posix.sep);
|
|
55
|
+
}
|
|
56
|
+
function stripSlashes(value) {
|
|
57
|
+
return value.replace(/^\/+|\/+$/g, '');
|
|
58
|
+
}
|
|
59
|
+
function titleFromPackageName(value) {
|
|
60
|
+
return value
|
|
61
|
+
.replace(/^@[^/]+\//, '')
|
|
62
|
+
.split(/[-_\s]+/)
|
|
63
|
+
.filter(Boolean)
|
|
64
|
+
.map((token) => token.charAt(0).toUpperCase() + token.slice(1))
|
|
65
|
+
.join(' ');
|
|
66
|
+
}
|
|
67
|
+
function removeExtension(filePath) {
|
|
68
|
+
return filePath.replace(/\.[cm]?[tj]sx?$/, '');
|
|
69
|
+
}
|
|
70
|
+
function routeToSegments(route) {
|
|
71
|
+
const stripped = stripSlashes(route);
|
|
72
|
+
return stripped ? stripped.split('/').filter(Boolean) : ['brandkit'];
|
|
73
|
+
}
|
|
74
|
+
function importPath(fromFile, toFile) {
|
|
75
|
+
let relative = toPosixPath(path.relative(path.dirname(fromFile), removeExtension(toFile)));
|
|
76
|
+
if (!relative.startsWith('.')) {
|
|
77
|
+
relative = `./${relative}`;
|
|
78
|
+
}
|
|
79
|
+
return relative;
|
|
80
|
+
}
|
|
81
|
+
async function pathExists(filePath) {
|
|
82
|
+
try {
|
|
83
|
+
await access(filePath);
|
|
84
|
+
return true;
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
async function readPackageJson(cwd) {
|
|
91
|
+
const packagePath = path.join(cwd, 'package.json');
|
|
92
|
+
try {
|
|
93
|
+
const source = await readFile(packagePath, 'utf8');
|
|
94
|
+
return {
|
|
95
|
+
path: packagePath,
|
|
96
|
+
value: JSON.parse(source),
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
async function readOwnPackageJson() {
|
|
104
|
+
const packagePath = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..', 'package.json');
|
|
105
|
+
try {
|
|
106
|
+
const source = await readFile(packagePath, 'utf8');
|
|
107
|
+
return JSON.parse(source);
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
return { name: 'open-brandkit', version: 'latest' };
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
async function findConfigPath(cwd, explicitPath) {
|
|
114
|
+
if (explicitPath)
|
|
115
|
+
return path.resolve(cwd, explicitPath);
|
|
116
|
+
for (const configName of defaultConfigNames) {
|
|
117
|
+
const candidate = path.join(cwd, configName);
|
|
118
|
+
if (await pathExists(candidate))
|
|
119
|
+
return candidate;
|
|
120
|
+
}
|
|
121
|
+
throw new Error(`Could not find ${defaultConfigNames.join(', ')}. Run open-brandkit init first.`);
|
|
122
|
+
}
|
|
123
|
+
async function loadConfig(configPath) {
|
|
124
|
+
const jiti = createJiti(pathToFileURL(configPath).href);
|
|
125
|
+
const loaded = (await jiti.import(configPath, {
|
|
126
|
+
default: true,
|
|
127
|
+
}));
|
|
128
|
+
if (loaded && typeof loaded === 'object' && 'default' in loaded) {
|
|
129
|
+
return loaded.default;
|
|
130
|
+
}
|
|
131
|
+
return loaded;
|
|
132
|
+
}
|
|
133
|
+
async function prompt(question, defaultValue, options) {
|
|
134
|
+
if (!options.enabled || !options.rl)
|
|
135
|
+
return defaultValue;
|
|
136
|
+
const answer = await options.rl.question(`${question} (${defaultValue}): `);
|
|
137
|
+
return answer.trim() || defaultValue;
|
|
138
|
+
}
|
|
139
|
+
async function promptBoolean(question, defaultValue, options) {
|
|
140
|
+
if (!options.enabled || !options.rl)
|
|
141
|
+
return defaultValue;
|
|
142
|
+
const suffix = defaultValue ? 'Y/n' : 'y/N';
|
|
143
|
+
const answer = (await options.rl.question(`${question} (${suffix}): `))
|
|
144
|
+
.trim()
|
|
145
|
+
.toLowerCase();
|
|
146
|
+
if (!answer)
|
|
147
|
+
return defaultValue;
|
|
148
|
+
return answer === 'y' || answer === 'yes';
|
|
149
|
+
}
|
|
150
|
+
async function detectLogoDir(cwd) {
|
|
151
|
+
const candidates = [
|
|
152
|
+
'public/brandkit-source/logos',
|
|
153
|
+
'public/brandkit/logos',
|
|
154
|
+
'public/logos',
|
|
155
|
+
'public/logo',
|
|
156
|
+
'assets/logos',
|
|
157
|
+
'src/assets/logos',
|
|
158
|
+
];
|
|
159
|
+
for (const candidate of candidates) {
|
|
160
|
+
if (await pathExists(path.join(cwd, candidate))) {
|
|
161
|
+
return candidate;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return 'public/brandkit-source/logos';
|
|
165
|
+
}
|
|
166
|
+
async function detectColorPath(cwd) {
|
|
167
|
+
const candidates = [
|
|
168
|
+
'docs/brand-colors.md',
|
|
169
|
+
'docs/colors.md',
|
|
170
|
+
'brand-colors.md',
|
|
171
|
+
'colors.md',
|
|
172
|
+
'brand-colors.json',
|
|
173
|
+
'colors.json',
|
|
174
|
+
'brand-colors.csv',
|
|
175
|
+
'colors.csv',
|
|
176
|
+
];
|
|
177
|
+
for (const candidate of candidates) {
|
|
178
|
+
if (await pathExists(path.join(cwd, candidate))) {
|
|
179
|
+
return candidate;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return 'docs/brand-colors.md';
|
|
183
|
+
}
|
|
184
|
+
async function detectAppDir(cwd, packageName) {
|
|
185
|
+
if (await pathExists(path.join(cwd, 'src/app')))
|
|
186
|
+
return 'src/app';
|
|
187
|
+
if (await pathExists(path.join(cwd, 'app')))
|
|
188
|
+
return 'app';
|
|
189
|
+
if (await pathExists(path.join(cwd, 'src')))
|
|
190
|
+
return 'src/app';
|
|
191
|
+
if (packageName)
|
|
192
|
+
return 'app';
|
|
193
|
+
return 'app';
|
|
194
|
+
}
|
|
195
|
+
function colorSourceFromPath(colorPath) {
|
|
196
|
+
const extension = path.extname(colorPath).toLowerCase();
|
|
197
|
+
if (extension === '.json') {
|
|
198
|
+
return { type: 'json', path: colorPath };
|
|
199
|
+
}
|
|
200
|
+
if (extension === '.csv' || extension === '.tsv') {
|
|
201
|
+
return { type: 'csv', path: colorPath };
|
|
202
|
+
}
|
|
203
|
+
return { type: 'markdown-table', path: colorPath };
|
|
204
|
+
}
|
|
205
|
+
function assetNameParts(filePath) {
|
|
206
|
+
return path
|
|
207
|
+
.basename(filePath, path.extname(filePath))
|
|
208
|
+
.toLowerCase()
|
|
209
|
+
.split(/[^a-z0-9]+/)
|
|
210
|
+
.filter(Boolean);
|
|
211
|
+
}
|
|
212
|
+
function isWordmarkPath(filePath) {
|
|
213
|
+
const parts = assetNameParts(filePath);
|
|
214
|
+
const compact = parts.join('');
|
|
215
|
+
return compact.includes('wordmark');
|
|
216
|
+
}
|
|
217
|
+
function isIconPath(filePath) {
|
|
218
|
+
const parts = assetNameParts(filePath);
|
|
219
|
+
const compact = parts.join('');
|
|
220
|
+
return (!isWordmarkPath(filePath) &&
|
|
221
|
+
(parts.some((part) => ['icon', 'icons', 'symbol', 'symbols', 'favicon', 'mark'].includes(part)) ||
|
|
222
|
+
compact.includes('brandmark')));
|
|
223
|
+
}
|
|
224
|
+
function isLogoPath(filePath) {
|
|
225
|
+
const parts = assetNameParts(filePath);
|
|
226
|
+
return (!isWordmarkPath(filePath) &&
|
|
227
|
+
!isIconPath(filePath) &&
|
|
228
|
+
parts.some((part) => ['logo', 'logos', 'lockup', 'lockups'].includes(part)));
|
|
229
|
+
}
|
|
230
|
+
const ignoredMarkVariantTokens = new Set([
|
|
231
|
+
'brand',
|
|
232
|
+
'brandkit',
|
|
233
|
+
'favicon',
|
|
234
|
+
'icon',
|
|
235
|
+
'icons',
|
|
236
|
+
'lockup',
|
|
237
|
+
'lockups',
|
|
238
|
+
'logo',
|
|
239
|
+
'logos',
|
|
240
|
+
'mark',
|
|
241
|
+
'marks',
|
|
242
|
+
'symbol',
|
|
243
|
+
'symbols',
|
|
244
|
+
'word',
|
|
245
|
+
'wordmark',
|
|
246
|
+
'wordmarks',
|
|
247
|
+
]);
|
|
248
|
+
const namedMarkVariantColors = {
|
|
249
|
+
black: { hex: '#05070b', label: 'Black' },
|
|
250
|
+
white: { hex: '#ffffff', label: 'White' },
|
|
251
|
+
};
|
|
252
|
+
function titleFromTokens(tokens) {
|
|
253
|
+
return tokens
|
|
254
|
+
.map((token) => token.charAt(0).toUpperCase() + token.slice(1))
|
|
255
|
+
.join(' ');
|
|
256
|
+
}
|
|
257
|
+
function slugFromTokens(tokens) {
|
|
258
|
+
return tokens.join('-').replace(/[^a-z0-9-]+/g, '-').replace(/^-+|-+$/g, '');
|
|
259
|
+
}
|
|
260
|
+
function normalizeHexColor(value) {
|
|
261
|
+
const trimmed = value.trim().toLowerCase();
|
|
262
|
+
const short = trimmed.match(/^#([0-9a-f]{3})$/i);
|
|
263
|
+
const long = trimmed.match(/^#([0-9a-f]{6})(?:[0-9a-f]{2})?$/i);
|
|
264
|
+
if (short) {
|
|
265
|
+
return `#${short[1]
|
|
266
|
+
.split('')
|
|
267
|
+
.map((character) => `${character}${character}`)
|
|
268
|
+
.join('')}`;
|
|
269
|
+
}
|
|
270
|
+
return long ? `#${long[1]}` : null;
|
|
271
|
+
}
|
|
272
|
+
function colorValueToHex(value) {
|
|
273
|
+
const normalized = normalizeHexColor(value);
|
|
274
|
+
if (normalized)
|
|
275
|
+
return normalized;
|
|
276
|
+
if (/^white$/i.test(value.trim()))
|
|
277
|
+
return '#ffffff';
|
|
278
|
+
if (/^black$/i.test(value.trim()))
|
|
279
|
+
return '#05070b';
|
|
280
|
+
return null;
|
|
281
|
+
}
|
|
282
|
+
function addColorWeight(weights, value, weight) {
|
|
283
|
+
if (!value || /^(none|transparent|currentcolor)$/i.test(value.trim()))
|
|
284
|
+
return;
|
|
285
|
+
const hex = colorValueToHex(value);
|
|
286
|
+
if (!hex)
|
|
287
|
+
return;
|
|
288
|
+
weights.set(hex, (weights.get(hex) ?? 0) + weight);
|
|
289
|
+
}
|
|
290
|
+
function inferSvgSwatch(source) {
|
|
291
|
+
const classFills = new Map();
|
|
292
|
+
const weights = new Map();
|
|
293
|
+
const classRulePattern = /\.([_a-zA-Z0-9-]+)\s*\{[^}]*?\bfill\s*:\s*([^;}]+)[^}]*\}/g;
|
|
294
|
+
const shapePattern = /<(path|polygon|circle|rect|ellipse|line|polyline)\b([^>]*)>/gi;
|
|
295
|
+
let classRule;
|
|
296
|
+
let shape;
|
|
297
|
+
while ((classRule = classRulePattern.exec(source))) {
|
|
298
|
+
classFills.set(classRule[1], classRule[2].trim());
|
|
299
|
+
}
|
|
300
|
+
while ((shape = shapePattern.exec(source))) {
|
|
301
|
+
const attributes = shape[2];
|
|
302
|
+
const fillAttribute = attributes.match(/\bfill=["']([^"']+)["']/i);
|
|
303
|
+
const styleFill = attributes.match(/\bstyle=["'][^"']*?\bfill\s*:\s*([^;"']+)/i);
|
|
304
|
+
const classAttribute = attributes.match(/\bclass=["']([^"']+)["']/i);
|
|
305
|
+
if (fillAttribute) {
|
|
306
|
+
addColorWeight(weights, fillAttribute[1], 8);
|
|
307
|
+
continue;
|
|
308
|
+
}
|
|
309
|
+
if (styleFill) {
|
|
310
|
+
addColorWeight(weights, styleFill[1], 8);
|
|
311
|
+
continue;
|
|
312
|
+
}
|
|
313
|
+
if (classAttribute) {
|
|
314
|
+
for (const className of classAttribute[1].split(/\s+/).filter(Boolean)) {
|
|
315
|
+
addColorWeight(weights, classFills.get(className), 8);
|
|
316
|
+
}
|
|
317
|
+
continue;
|
|
318
|
+
}
|
|
319
|
+
addColorWeight(weights, 'black', 1);
|
|
320
|
+
}
|
|
321
|
+
source.replace(/\bfill\s*:\s*([^;}]+)/gi, (_match, color) => {
|
|
322
|
+
addColorWeight(weights, color, 1);
|
|
323
|
+
return _match;
|
|
324
|
+
});
|
|
325
|
+
source.replace(/\bfill=["']([^"']+)["']/gi, (_match, color) => {
|
|
326
|
+
addColorWeight(weights, color, 1);
|
|
327
|
+
return _match;
|
|
328
|
+
});
|
|
329
|
+
return Array.from(weights.entries()).sort((left, right) => right[1] - left[1])[0]?.[0];
|
|
330
|
+
}
|
|
331
|
+
async function inferAssetSwatch(cwd, assetPath) {
|
|
332
|
+
if (path.extname(assetPath).toLowerCase() !== '.svg')
|
|
333
|
+
return '#6b7280';
|
|
334
|
+
try {
|
|
335
|
+
return inferSvgSwatch(await readFile(path.resolve(cwd, assetPath), 'utf8')) ?? '#6b7280';
|
|
336
|
+
}
|
|
337
|
+
catch {
|
|
338
|
+
return '#6b7280';
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
function commonAssetParts(filePaths) {
|
|
342
|
+
const partSets = filePaths.map((filePath) => new Set(assetNameParts(filePath)));
|
|
343
|
+
const [firstParts] = partSets;
|
|
344
|
+
if (!firstParts)
|
|
345
|
+
return new Set();
|
|
346
|
+
return new Set(Array.from(firstParts).filter((part) => partSets.every((parts) => parts.has(part))));
|
|
347
|
+
}
|
|
348
|
+
function variantPartsForAsset(filePath, commonParts) {
|
|
349
|
+
return assetNameParts(filePath).filter((part) => !commonParts.has(part) && !ignoredMarkVariantTokens.has(part));
|
|
350
|
+
}
|
|
351
|
+
function uniqueKey(baseKey, usedKeys) {
|
|
352
|
+
const fallback = baseKey || 'variant';
|
|
353
|
+
let key = fallback;
|
|
354
|
+
let index = 2;
|
|
355
|
+
while (usedKeys.has(key)) {
|
|
356
|
+
key = `${fallback}-${index}`;
|
|
357
|
+
index += 1;
|
|
358
|
+
}
|
|
359
|
+
usedKeys.add(key);
|
|
360
|
+
return key;
|
|
361
|
+
}
|
|
362
|
+
async function inferBannerMarkVariants(cwd, logoDir, colors) {
|
|
363
|
+
const absoluteLogoDir = path.resolve(cwd, logoDir);
|
|
364
|
+
const files = await readBrandAssetFiles(absoluteLogoDir);
|
|
365
|
+
const relativeFiles = files.map((file) => toPosixPath(path.relative(cwd, file)));
|
|
366
|
+
const svgFiles = relativeFiles.filter((file) => path.extname(file).toLowerCase() === '.svg');
|
|
367
|
+
const candidates = svgFiles.length ? svgFiles : relativeFiles;
|
|
368
|
+
const variants = [];
|
|
369
|
+
function colorAliases(color) {
|
|
370
|
+
const parts = `${color.key} ${color.label}`
|
|
371
|
+
.toLowerCase()
|
|
372
|
+
.split(/[^a-z0-9]+/)
|
|
373
|
+
.filter(Boolean)
|
|
374
|
+
.filter((part) => part.length >= 3 && !['brand', 'color', 'colour'].includes(part));
|
|
375
|
+
if (/^#(?:fff|ffffff)$/i.test(color.hex))
|
|
376
|
+
parts.push('white');
|
|
377
|
+
if (/^#(?:000|000000|05070b)$/i.test(color.hex))
|
|
378
|
+
parts.push('black');
|
|
379
|
+
return Array.from(new Set(parts));
|
|
380
|
+
}
|
|
381
|
+
function colorForParts(parts) {
|
|
382
|
+
return colors.find((color) => colorAliases(color).some((alias) => parts.includes(alias)));
|
|
383
|
+
}
|
|
384
|
+
async function inferColorAssetChoices(markCandidates) {
|
|
385
|
+
const commonParts = commonAssetParts(markCandidates);
|
|
386
|
+
const usedKeys = new Set();
|
|
387
|
+
const choices = await Promise.all(markCandidates.map(async (candidate) => {
|
|
388
|
+
const parts = variantPartsForAsset(candidate, commonParts);
|
|
389
|
+
const color = colorForParts(parts);
|
|
390
|
+
const namedColorToken = Object.keys(namedMarkVariantColors).find((token) => parts.includes(token));
|
|
391
|
+
const namedColor = namedColorToken
|
|
392
|
+
? namedMarkVariantColors[namedColorToken]
|
|
393
|
+
: null;
|
|
394
|
+
const variantKey = parts.length ? slugFromTokens(parts) : 'default';
|
|
395
|
+
const keyBase = color?.key ??
|
|
396
|
+
namedColorToken ??
|
|
397
|
+
variantKey;
|
|
398
|
+
const key = uniqueKey(keyBase, usedKeys);
|
|
399
|
+
const label = color?.label ??
|
|
400
|
+
namedColor?.label ??
|
|
401
|
+
(parts.length ? titleFromTokens(parts) : 'Default');
|
|
402
|
+
const hex = color?.hex ??
|
|
403
|
+
namedColor?.hex ??
|
|
404
|
+
(await inferAssetSwatch(cwd, candidate));
|
|
405
|
+
const colorIndex = color ? colors.indexOf(color) : -1;
|
|
406
|
+
const namedIndex = namedColorToken
|
|
407
|
+
? Object.keys(namedMarkVariantColors).indexOf(namedColorToken)
|
|
408
|
+
: -1;
|
|
409
|
+
const rank = parts.length === 0
|
|
410
|
+
? 0
|
|
411
|
+
: colorIndex >= 0
|
|
412
|
+
? 10 + colorIndex
|
|
413
|
+
: namedIndex >= 0
|
|
414
|
+
? 50 + namedIndex
|
|
415
|
+
: 100;
|
|
416
|
+
return {
|
|
417
|
+
assetPath: candidate,
|
|
418
|
+
option: { hex, key, label },
|
|
419
|
+
rank,
|
|
420
|
+
};
|
|
421
|
+
}));
|
|
422
|
+
choices.sort((left, right) => left.rank - right.rank ||
|
|
423
|
+
left.option.label.localeCompare(right.option.label) ||
|
|
424
|
+
left.assetPath.localeCompare(right.assetPath));
|
|
425
|
+
const colorAssets = {};
|
|
426
|
+
const colorOptions = choices.map((choice) => {
|
|
427
|
+
colorAssets[choice.option.key] = choice.assetPath;
|
|
428
|
+
return choice.option;
|
|
429
|
+
});
|
|
430
|
+
return {
|
|
431
|
+
assetPath: choices[0]?.assetPath ?? markCandidates[0],
|
|
432
|
+
colorAssets,
|
|
433
|
+
colorOptions,
|
|
434
|
+
};
|
|
435
|
+
}
|
|
436
|
+
async function addVariant({ key, label, markCandidates, scale, }) {
|
|
437
|
+
if (!markCandidates.length)
|
|
438
|
+
return;
|
|
439
|
+
const { assetPath, colorAssets, colorOptions } = await inferColorAssetChoices(markCandidates);
|
|
440
|
+
const colorKeys = colorOptions.map((option) => option.key);
|
|
441
|
+
variants.push({
|
|
442
|
+
key,
|
|
443
|
+
label,
|
|
444
|
+
assetPath,
|
|
445
|
+
...(colorKeys.length ? { colorAssets, colorKeys, colorOptions } : {}),
|
|
446
|
+
scale,
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
await addVariant({
|
|
450
|
+
key: 'logo',
|
|
451
|
+
label: 'Logo',
|
|
452
|
+
markCandidates: candidates.filter(isLogoPath),
|
|
453
|
+
scale: 0.34,
|
|
454
|
+
});
|
|
455
|
+
await addVariant({
|
|
456
|
+
key: 'wordmark',
|
|
457
|
+
label: 'Wordmark',
|
|
458
|
+
markCandidates: candidates.filter(isWordmarkPath),
|
|
459
|
+
scale: 0.26,
|
|
460
|
+
});
|
|
461
|
+
await addVariant({
|
|
462
|
+
key: 'icon',
|
|
463
|
+
label: 'Icon',
|
|
464
|
+
markCandidates: candidates.filter(isIconPath),
|
|
465
|
+
scale: 0.18,
|
|
466
|
+
});
|
|
467
|
+
if (!variants.length) {
|
|
468
|
+
variants.push({
|
|
469
|
+
assetPath: candidates[0] ?? path.posix.join(toPosixPath(logoDir), 'logo.svg'),
|
|
470
|
+
key: 'logo',
|
|
471
|
+
label: 'Logo',
|
|
472
|
+
scale: 0.34,
|
|
473
|
+
});
|
|
474
|
+
}
|
|
475
|
+
return variants;
|
|
476
|
+
}
|
|
477
|
+
async function inferBannerColors(cwd, colorSource) {
|
|
478
|
+
let colors = [];
|
|
479
|
+
try {
|
|
480
|
+
colors = await loadBrandKitColors({ sources: [colorSource] }, cwd);
|
|
481
|
+
}
|
|
482
|
+
catch {
|
|
483
|
+
colors = [];
|
|
484
|
+
}
|
|
485
|
+
const [first, second, third] = colors;
|
|
486
|
+
return [
|
|
487
|
+
{
|
|
488
|
+
key: 'primary',
|
|
489
|
+
label: first?.name ?? 'Primary',
|
|
490
|
+
hex: first?.hex ?? '#0d2249',
|
|
491
|
+
},
|
|
492
|
+
{
|
|
493
|
+
key: 'accent',
|
|
494
|
+
label: second?.name ?? 'Accent',
|
|
495
|
+
hex: second?.hex ?? '#4784de',
|
|
496
|
+
},
|
|
497
|
+
{
|
|
498
|
+
key: 'light',
|
|
499
|
+
label: third?.name ?? 'Light',
|
|
500
|
+
hex: third?.hex ?? '#ffffff',
|
|
501
|
+
},
|
|
502
|
+
];
|
|
503
|
+
}
|
|
504
|
+
async function collectInitAnswers(cwd, args) {
|
|
505
|
+
const yes = hasFlag(args, '--yes') || hasFlag(args, '-y');
|
|
506
|
+
const interactive = !yes && Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
507
|
+
const rl = interactive
|
|
508
|
+
? createInterface({ input: process.stdin, output: process.stdout })
|
|
509
|
+
: null;
|
|
510
|
+
const packageJson = await readPackageJson(cwd);
|
|
511
|
+
const defaultBrandName = packageJson?.value.name
|
|
512
|
+
? titleFromPackageName(packageJson.value.name)
|
|
513
|
+
: 'Acme Studio';
|
|
514
|
+
if (hasOption(args, '--framework')) {
|
|
515
|
+
rl?.close();
|
|
516
|
+
throw new Error('Unknown option: --framework. Open BrandKit init currently supports Next.js App Router projects only. Additional installation paths are planned.');
|
|
517
|
+
}
|
|
518
|
+
const brandName = await prompt('Brand name', getFlagValue(args, '--brand') ?? defaultBrandName, { enabled: interactive && !getFlagValue(args, '--brand'), rl });
|
|
519
|
+
const shortName = await prompt('Short brand name', getFlagValue(args, '--short-name') ?? brandName.split(/\s+/)[0] ?? brandName, { enabled: interactive && !getFlagValue(args, '--short-name'), rl });
|
|
520
|
+
const logoDir = await prompt('Logo directory', getFlagValue(args, '--logos') ?? (await detectLogoDir(cwd)), { enabled: interactive && !getFlagValue(args, '--logos'), rl });
|
|
521
|
+
const colorPath = await prompt('Colors file', getFlagValue(args, '--colors') ?? (await detectColorPath(cwd)), { enabled: interactive && !getFlagValue(args, '--colors'), rl });
|
|
522
|
+
const route = await prompt('Brand Kit route', getFlagValue(args, '--route') ?? '/brandkit', {
|
|
523
|
+
enabled: interactive && !getFlagValue(args, '--route'),
|
|
524
|
+
rl,
|
|
525
|
+
});
|
|
526
|
+
const appDir = await prompt('Next app directory', getFlagValue(args, '--app-dir') ??
|
|
527
|
+
(await detectAppDir(cwd, packageJson?.value.name)), { enabled: interactive && !getFlagValue(args, '--app-dir'), rl });
|
|
528
|
+
const configPath = path.resolve(cwd, getFlagValue(args, '--config') ?? 'brandkit.config.ts');
|
|
529
|
+
const shouldBuild = hasFlag(args, '--build') ||
|
|
530
|
+
(yes && !hasFlag(args, '--no-build')) ||
|
|
531
|
+
(await promptBoolean('Run build now', true, {
|
|
532
|
+
enabled: interactive && !hasFlag(args, '--no-build'),
|
|
533
|
+
rl,
|
|
534
|
+
}));
|
|
535
|
+
rl?.close();
|
|
536
|
+
return {
|
|
537
|
+
appDir,
|
|
538
|
+
brandName,
|
|
539
|
+
colorPath: toPosixPath(colorPath),
|
|
540
|
+
configPath,
|
|
541
|
+
logoDir: toPosixPath(logoDir),
|
|
542
|
+
route,
|
|
543
|
+
shortName,
|
|
544
|
+
shouldBuild,
|
|
545
|
+
};
|
|
546
|
+
}
|
|
547
|
+
async function makeConfig(cwd, answers) {
|
|
548
|
+
const colorSource = colorSourceFromPath(answers.colorPath);
|
|
549
|
+
const bannerColors = await inferBannerColors(cwd, colorSource);
|
|
550
|
+
const bannerMarkVariants = await inferBannerMarkVariants(cwd, answers.logoDir, bannerColors);
|
|
551
|
+
const defaultMarkColor = bannerMarkVariants[0]?.colorOptions?.[0]?.key ??
|
|
552
|
+
bannerMarkVariants[0]?.colorKeys?.[0] ??
|
|
553
|
+
'light';
|
|
554
|
+
return {
|
|
555
|
+
brand: {
|
|
556
|
+
name: answers.brandName,
|
|
557
|
+
shortName: answers.shortName,
|
|
558
|
+
description: `Approved assets, colors, avatars, and social banners for ${answers.brandName}.`,
|
|
559
|
+
},
|
|
560
|
+
route: answers.route,
|
|
561
|
+
logos: {
|
|
562
|
+
sourceDir: answers.logoDir,
|
|
563
|
+
groups: [
|
|
564
|
+
{
|
|
565
|
+
key: 'logo-lockups',
|
|
566
|
+
label: 'Logo Lockups',
|
|
567
|
+
match: ['logo'],
|
|
568
|
+
description: 'Primary logo lockups in approved colorways.',
|
|
569
|
+
},
|
|
570
|
+
{
|
|
571
|
+
key: 'wordmarks',
|
|
572
|
+
label: 'Wordmarks',
|
|
573
|
+
match: ['wordmark', 'word-mark', 'word'],
|
|
574
|
+
description: 'Text-first marks for wide placements.',
|
|
575
|
+
},
|
|
576
|
+
{
|
|
577
|
+
key: 'icons',
|
|
578
|
+
label: 'Icons',
|
|
579
|
+
match: ['icon', 'symbol', 'favicon', 'brand-mark'],
|
|
580
|
+
description: 'Symbol-only marks for compact surfaces.',
|
|
581
|
+
},
|
|
582
|
+
],
|
|
583
|
+
},
|
|
584
|
+
colors: {
|
|
585
|
+
sources: [colorSource],
|
|
586
|
+
},
|
|
587
|
+
socialBanners: {
|
|
588
|
+
markVariants: bannerMarkVariants,
|
|
589
|
+
colors: bannerColors,
|
|
590
|
+
presets: [
|
|
591
|
+
{
|
|
592
|
+
key: 'x-profile-header',
|
|
593
|
+
label: 'X / Twitter profile header',
|
|
594
|
+
width: 1500,
|
|
595
|
+
height: 500,
|
|
596
|
+
backgroundColor: 'primary',
|
|
597
|
+
accentColor: 'accent',
|
|
598
|
+
markColor: defaultMarkColor,
|
|
599
|
+
secondaryColor: 'light',
|
|
600
|
+
pattern: 'diagonal-sweep',
|
|
601
|
+
},
|
|
602
|
+
{
|
|
603
|
+
key: 'linkedin-personal-background',
|
|
604
|
+
label: 'LinkedIn personal background',
|
|
605
|
+
width: 1584,
|
|
606
|
+
height: 396,
|
|
607
|
+
backgroundColor: 'primary',
|
|
608
|
+
accentColor: 'accent',
|
|
609
|
+
markColor: defaultMarkColor,
|
|
610
|
+
secondaryColor: 'light',
|
|
611
|
+
pattern: 'radial-glow',
|
|
612
|
+
},
|
|
613
|
+
{
|
|
614
|
+
key: 'linkedin-organization-cover',
|
|
615
|
+
label: 'LinkedIn organization cover',
|
|
616
|
+
width: 4200,
|
|
617
|
+
height: 700,
|
|
618
|
+
backgroundColor: 'primary',
|
|
619
|
+
accentColor: 'accent',
|
|
620
|
+
markColor: defaultMarkColor,
|
|
621
|
+
secondaryColor: 'light',
|
|
622
|
+
pattern: 'wave',
|
|
623
|
+
},
|
|
624
|
+
{
|
|
625
|
+
key: 'facebook-page-cover',
|
|
626
|
+
label: 'Facebook page cover',
|
|
627
|
+
width: 851,
|
|
628
|
+
height: 315,
|
|
629
|
+
backgroundColor: 'primary',
|
|
630
|
+
accentColor: 'accent',
|
|
631
|
+
markColor: defaultMarkColor,
|
|
632
|
+
secondaryColor: 'light',
|
|
633
|
+
pattern: 'split-field',
|
|
634
|
+
},
|
|
635
|
+
],
|
|
636
|
+
},
|
|
637
|
+
};
|
|
638
|
+
}
|
|
639
|
+
function configSource(config) {
|
|
640
|
+
return `import type { BrandKitConfig } from 'open-brandkit'
|
|
641
|
+
|
|
642
|
+
export default ${JSON.stringify(config, null, 2)} satisfies BrandKitConfig
|
|
643
|
+
`;
|
|
644
|
+
}
|
|
645
|
+
async function writeGeneratedFile({ content, created, filePath, force, skipped, }) {
|
|
646
|
+
if ((await pathExists(filePath)) && !force) {
|
|
647
|
+
skipped.push(filePath);
|
|
648
|
+
return;
|
|
649
|
+
}
|
|
650
|
+
await mkdir(path.dirname(filePath), { recursive: true });
|
|
651
|
+
await writeFile(filePath, content);
|
|
652
|
+
created.push(filePath);
|
|
653
|
+
}
|
|
654
|
+
function pageRouteSource(configImport) {
|
|
655
|
+
return `import { BrandKitPage, getBrandKitNextPageProps } from 'open-brandkit/next'
|
|
656
|
+
|
|
657
|
+
import config from '${configImport}'
|
|
658
|
+
|
|
659
|
+
export default async function BrandKitRoute() {
|
|
660
|
+
const props = await getBrandKitNextPageProps(config)
|
|
661
|
+
const route = config.route ?? '/brandkit'
|
|
662
|
+
|
|
663
|
+
return (
|
|
664
|
+
<BrandKitPage
|
|
665
|
+
{...props}
|
|
666
|
+
endpoints={{
|
|
667
|
+
bannerPresets: \`\${route}/banners/presets\`,
|
|
668
|
+
bannerUpload: \`\${route}/banners\`,
|
|
669
|
+
favicon: \`\${route}/favicon\`,
|
|
670
|
+
}}
|
|
671
|
+
/>
|
|
672
|
+
)
|
|
673
|
+
}
|
|
674
|
+
`;
|
|
675
|
+
}
|
|
676
|
+
function faviconRouteSource(configImport) {
|
|
677
|
+
return `import { createBrandKitFaviconHandler } from 'open-brandkit/next/server'
|
|
678
|
+
|
|
679
|
+
import config from '${configImport}'
|
|
680
|
+
|
|
681
|
+
export const runtime = 'nodejs'
|
|
682
|
+
export const { POST } = createBrandKitFaviconHandler(config)
|
|
683
|
+
`;
|
|
684
|
+
}
|
|
685
|
+
function bannerUploadRouteSource(configImport) {
|
|
686
|
+
return `import { createBrandKitBannerUploadHandler } from 'open-brandkit/next/server'
|
|
687
|
+
|
|
688
|
+
import config from '${configImport}'
|
|
689
|
+
|
|
690
|
+
export const runtime = 'nodejs'
|
|
691
|
+
export const { POST } = createBrandKitBannerUploadHandler(config)
|
|
692
|
+
`;
|
|
693
|
+
}
|
|
694
|
+
function bannerPresetRouteSource(configImport) {
|
|
695
|
+
return `import { createBrandKitBannerPresetHandler } from 'open-brandkit/next/server'
|
|
696
|
+
|
|
697
|
+
import config from '${configImport}'
|
|
698
|
+
|
|
699
|
+
export const runtime = 'nodejs'
|
|
700
|
+
export const { POST } = createBrandKitBannerPresetHandler(config)
|
|
701
|
+
`;
|
|
702
|
+
}
|
|
703
|
+
function downloadRouteSource() {
|
|
704
|
+
return `import { createBrandKitDownloadHandler } from 'open-brandkit/next/server'
|
|
705
|
+
|
|
706
|
+
export const runtime = 'nodejs'
|
|
707
|
+
export const { GET } = createBrandKitDownloadHandler()
|
|
708
|
+
`;
|
|
709
|
+
}
|
|
710
|
+
function layoutRouteSource() {
|
|
711
|
+
return `import type { Metadata } from 'next'
|
|
712
|
+
import type { ReactNode } from 'react'
|
|
713
|
+
|
|
714
|
+
export const metadata: Metadata = {
|
|
715
|
+
title: 'Brand Kit',
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
export default function BrandKitLayout({ children }: { children: ReactNode }) {
|
|
719
|
+
return children
|
|
720
|
+
}
|
|
721
|
+
`;
|
|
722
|
+
}
|
|
723
|
+
async function writeNextAdapterFiles({ answers, configPath, created, cwd, force, skipped, }) {
|
|
724
|
+
const routeDir = path.join(cwd, answers.appDir, ...routeToSegments(answers.route));
|
|
725
|
+
const pagePath = path.join(routeDir, 'page.tsx');
|
|
726
|
+
const faviconPath = path.join(routeDir, 'favicon', 'route.ts');
|
|
727
|
+
const bannerUploadPath = path.join(routeDir, 'banners', 'route.ts');
|
|
728
|
+
const bannerPresetPath = path.join(routeDir, 'banners', 'presets', 'route.ts');
|
|
729
|
+
const downloadPath = path.join(routeDir, 'download', '[group]', 'route.ts');
|
|
730
|
+
const layoutPath = path.join(routeDir, 'layout.tsx');
|
|
731
|
+
await writeGeneratedFile({
|
|
732
|
+
content: pageRouteSource(importPath(pagePath, configPath)),
|
|
733
|
+
created,
|
|
734
|
+
filePath: pagePath,
|
|
735
|
+
force,
|
|
736
|
+
skipped,
|
|
737
|
+
});
|
|
738
|
+
await writeGeneratedFile({
|
|
739
|
+
content: faviconRouteSource(importPath(faviconPath, configPath)),
|
|
740
|
+
created,
|
|
741
|
+
filePath: faviconPath,
|
|
742
|
+
force,
|
|
743
|
+
skipped,
|
|
744
|
+
});
|
|
745
|
+
await writeGeneratedFile({
|
|
746
|
+
content: bannerUploadRouteSource(importPath(bannerUploadPath, configPath)),
|
|
747
|
+
created,
|
|
748
|
+
filePath: bannerUploadPath,
|
|
749
|
+
force,
|
|
750
|
+
skipped,
|
|
751
|
+
});
|
|
752
|
+
await writeGeneratedFile({
|
|
753
|
+
content: bannerPresetRouteSource(importPath(bannerPresetPath, configPath)),
|
|
754
|
+
created,
|
|
755
|
+
filePath: bannerPresetPath,
|
|
756
|
+
force,
|
|
757
|
+
skipped,
|
|
758
|
+
});
|
|
759
|
+
await writeGeneratedFile({
|
|
760
|
+
content: downloadRouteSource(),
|
|
761
|
+
created,
|
|
762
|
+
filePath: downloadPath,
|
|
763
|
+
force,
|
|
764
|
+
skipped,
|
|
765
|
+
});
|
|
766
|
+
await writeGeneratedFile({
|
|
767
|
+
content: layoutRouteSource(),
|
|
768
|
+
created,
|
|
769
|
+
filePath: layoutPath,
|
|
770
|
+
force,
|
|
771
|
+
skipped,
|
|
772
|
+
});
|
|
773
|
+
}
|
|
774
|
+
function getTailwindSourceDirective(cssPath, cwd) {
|
|
775
|
+
let sourcePath = toPosixPath(path.relative(path.dirname(cssPath), path.join(cwd, 'node_modules', 'open-brandkit', 'dist')));
|
|
776
|
+
if (!sourcePath.startsWith('.')) {
|
|
777
|
+
sourcePath = `./${sourcePath}`;
|
|
778
|
+
}
|
|
779
|
+
return `@source "${sourcePath}";`;
|
|
780
|
+
}
|
|
781
|
+
function getTailwindContentGlob(configPath, cwd) {
|
|
782
|
+
let sourcePath = toPosixPath(path.relative(path.dirname(configPath), path.join(cwd, 'node_modules', 'open-brandkit', 'dist')));
|
|
783
|
+
if (!sourcePath.startsWith('.')) {
|
|
784
|
+
sourcePath = `./${sourcePath}`;
|
|
785
|
+
}
|
|
786
|
+
return `${sourcePath}/**/*.{js,mjs}`;
|
|
787
|
+
}
|
|
788
|
+
function resolveCssImportPath(cwd, fromFile, importPath) {
|
|
789
|
+
if (importPath.startsWith('@/')) {
|
|
790
|
+
return path.join(cwd, 'src', importPath.slice(2));
|
|
791
|
+
}
|
|
792
|
+
if (importPath.startsWith('~/')) {
|
|
793
|
+
return path.join(cwd, importPath.slice(2));
|
|
794
|
+
}
|
|
795
|
+
if (importPath.startsWith('/')) {
|
|
796
|
+
return path.join(cwd, importPath.slice(1));
|
|
797
|
+
}
|
|
798
|
+
if (importPath.startsWith('.')) {
|
|
799
|
+
return path.resolve(path.dirname(fromFile), importPath);
|
|
800
|
+
}
|
|
801
|
+
return null;
|
|
802
|
+
}
|
|
803
|
+
function getCssImports(source) {
|
|
804
|
+
const imports = [];
|
|
805
|
+
const importPattern = /import\s+(?:[^'"]+\s+from\s+)?['"]([^'"]+\.css)['"]/g;
|
|
806
|
+
let match;
|
|
807
|
+
while ((match = importPattern.exec(source))) {
|
|
808
|
+
imports.push(match[1]);
|
|
809
|
+
}
|
|
810
|
+
return imports;
|
|
811
|
+
}
|
|
812
|
+
function isTailwindCssSource(source) {
|
|
813
|
+
return (source.includes('@import "tailwindcss"') ||
|
|
814
|
+
source.includes("@import 'tailwindcss'"));
|
|
815
|
+
}
|
|
816
|
+
async function findImportedTailwindCssPath(cwd, appDir) {
|
|
817
|
+
const layoutCandidates = [
|
|
818
|
+
path.join(cwd, appDir, 'layout.tsx'),
|
|
819
|
+
path.join(cwd, appDir, 'layout.jsx'),
|
|
820
|
+
path.join(cwd, appDir, 'layout.ts'),
|
|
821
|
+
path.join(cwd, appDir, 'layout.js'),
|
|
822
|
+
path.join(cwd, 'src/app/layout.tsx'),
|
|
823
|
+
path.join(cwd, 'src/app/layout.jsx'),
|
|
824
|
+
path.join(cwd, 'src/app/layout.ts'),
|
|
825
|
+
path.join(cwd, 'src/app/layout.js'),
|
|
826
|
+
path.join(cwd, 'app/layout.tsx'),
|
|
827
|
+
path.join(cwd, 'app/layout.jsx'),
|
|
828
|
+
path.join(cwd, 'app/layout.ts'),
|
|
829
|
+
path.join(cwd, 'app/layout.js'),
|
|
830
|
+
];
|
|
831
|
+
for (const layoutPath of Array.from(new Set(layoutCandidates))) {
|
|
832
|
+
if (!(await pathExists(layoutPath)))
|
|
833
|
+
continue;
|
|
834
|
+
const layoutSource = await readFile(layoutPath, 'utf8');
|
|
835
|
+
for (const cssImport of getCssImports(layoutSource)) {
|
|
836
|
+
const cssPath = resolveCssImportPath(cwd, layoutPath, cssImport);
|
|
837
|
+
if (!cssPath || !(await pathExists(cssPath)))
|
|
838
|
+
continue;
|
|
839
|
+
const cssSource = await readFile(cssPath, 'utf8');
|
|
840
|
+
if (isTailwindCssSource(cssSource))
|
|
841
|
+
return cssPath;
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
return null;
|
|
845
|
+
}
|
|
846
|
+
async function findTailwindCssPath(cwd, appDir) {
|
|
847
|
+
const importedCssPath = await findImportedTailwindCssPath(cwd, appDir);
|
|
848
|
+
if (importedCssPath)
|
|
849
|
+
return importedCssPath;
|
|
850
|
+
const candidates = [
|
|
851
|
+
path.join(cwd, appDir, 'globals.css'),
|
|
852
|
+
path.join(cwd, appDir, 'global.css'),
|
|
853
|
+
path.join(cwd, 'src/app/globals.css'),
|
|
854
|
+
path.join(cwd, 'src/app/global.css'),
|
|
855
|
+
path.join(cwd, 'app/globals.css'),
|
|
856
|
+
path.join(cwd, 'app/global.css'),
|
|
857
|
+
path.join(cwd, 'src/styles/tailwind.css'),
|
|
858
|
+
path.join(cwd, 'src/styles/globals.css'),
|
|
859
|
+
path.join(cwd, 'styles/tailwind.css'),
|
|
860
|
+
path.join(cwd, 'styles/globals.css'),
|
|
861
|
+
];
|
|
862
|
+
for (const candidate of Array.from(new Set(candidates))) {
|
|
863
|
+
if (!(await pathExists(candidate)))
|
|
864
|
+
continue;
|
|
865
|
+
const source = await readFile(candidate, 'utf8');
|
|
866
|
+
if (isTailwindCssSource(source)) {
|
|
867
|
+
return candidate;
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
return null;
|
|
871
|
+
}
|
|
872
|
+
async function findTailwindConfigPaths(cwd) {
|
|
873
|
+
const candidates = [
|
|
874
|
+
'tailwind.config.ts',
|
|
875
|
+
'tailwind.config.js',
|
|
876
|
+
'tailwind.config.mjs',
|
|
877
|
+
'tailwind.config.cjs',
|
|
878
|
+
].map((fileName) => path.join(cwd, fileName));
|
|
879
|
+
const existing = [];
|
|
880
|
+
for (const candidate of candidates) {
|
|
881
|
+
if (await pathExists(candidate))
|
|
882
|
+
existing.push(candidate);
|
|
883
|
+
}
|
|
884
|
+
return existing;
|
|
885
|
+
}
|
|
886
|
+
async function updateTailwindConfigContent({ created, cwd, skipped, }) {
|
|
887
|
+
const configPaths = await findTailwindConfigPaths(cwd);
|
|
888
|
+
let updated = false;
|
|
889
|
+
for (const configPath of configPaths) {
|
|
890
|
+
const source = await readFile(configPath, 'utf8');
|
|
891
|
+
const contentGlob = getTailwindContentGlob(configPath, cwd);
|
|
892
|
+
if (source.includes('open-brandkit/dist')) {
|
|
893
|
+
skipped.push(configPath);
|
|
894
|
+
updated = true;
|
|
895
|
+
continue;
|
|
896
|
+
}
|
|
897
|
+
const nextSource = source.replace(/(content\s*:\s*\[)([\s\S]*?)(\n\s*\])/, (_match, start, body, end) => `${start}${body}\n '${contentGlob}',${end}`);
|
|
898
|
+
if (nextSource === source)
|
|
899
|
+
continue;
|
|
900
|
+
await writeFile(configPath, nextSource);
|
|
901
|
+
created.push(configPath);
|
|
902
|
+
updated = true;
|
|
903
|
+
}
|
|
904
|
+
return updated;
|
|
905
|
+
}
|
|
906
|
+
async function updateTailwindSource({ appDir, created, cwd, skipped, }) {
|
|
907
|
+
if (await updateTailwindConfigContent({ created, cwd, skipped })) {
|
|
908
|
+
return;
|
|
909
|
+
}
|
|
910
|
+
const cssPath = await findTailwindCssPath(cwd, appDir);
|
|
911
|
+
if (!cssPath) {
|
|
912
|
+
skipped.push('Tailwind source configuration');
|
|
913
|
+
return;
|
|
914
|
+
}
|
|
915
|
+
const source = await readFile(cssPath, 'utf8');
|
|
916
|
+
const directive = getTailwindSourceDirective(cssPath, cwd);
|
|
917
|
+
if (source.includes('open-brandkit') || source.includes(directive)) {
|
|
918
|
+
skipped.push(cssPath);
|
|
919
|
+
return;
|
|
920
|
+
}
|
|
921
|
+
const nextSource = `${source.trimEnd()}\n${directive}\n`;
|
|
922
|
+
await writeFile(cssPath, nextSource);
|
|
923
|
+
created.push(cssPath);
|
|
924
|
+
}
|
|
925
|
+
async function updatePackageJson({ created, cwd, force, skipped, }) {
|
|
926
|
+
const packageJson = await readPackageJson(cwd);
|
|
927
|
+
if (!packageJson)
|
|
928
|
+
return;
|
|
929
|
+
const ownPackage = await readOwnPackageJson();
|
|
930
|
+
const packageName = ownPackage.name ?? 'open-brandkit';
|
|
931
|
+
const packageVersion = ownPackage.version && ownPackage.version !== '0.0.0'
|
|
932
|
+
? `^${ownPackage.version}`
|
|
933
|
+
: 'latest';
|
|
934
|
+
const scripts = packageJson.value.scripts ?? {};
|
|
935
|
+
const dependencies = packageJson.value.dependencies ?? {};
|
|
936
|
+
const devDependencies = packageJson.value.devDependencies ?? {};
|
|
937
|
+
const nextPackageValue = {
|
|
938
|
+
...packageJson.value,
|
|
939
|
+
scripts: {
|
|
940
|
+
...scripts,
|
|
941
|
+
'brandkit:build': scripts['brandkit:build'] && !force
|
|
942
|
+
? scripts['brandkit:build']
|
|
943
|
+
: 'open-brandkit build',
|
|
944
|
+
},
|
|
945
|
+
dependencies: packageJson.value.name === packageName || dependencies[packageName]
|
|
946
|
+
? dependencies
|
|
947
|
+
: {
|
|
948
|
+
...dependencies,
|
|
949
|
+
[packageName]: packageVersion,
|
|
950
|
+
},
|
|
951
|
+
devDependencies: dependencies.tailwindcss || devDependencies.tailwindcss
|
|
952
|
+
? devDependencies
|
|
953
|
+
: {
|
|
954
|
+
...devDependencies,
|
|
955
|
+
'@tailwindcss/postcss': '^4.0.0',
|
|
956
|
+
tailwindcss: '^4.0.0',
|
|
957
|
+
},
|
|
958
|
+
};
|
|
959
|
+
if (JSON.stringify(nextPackageValue) === JSON.stringify(packageJson.value)) {
|
|
960
|
+
skipped.push(packageJson.path);
|
|
961
|
+
return;
|
|
962
|
+
}
|
|
963
|
+
await writeFile(packageJson.path, `${JSON.stringify(nextPackageValue, null, 2)}\n`);
|
|
964
|
+
created.push(packageJson.path);
|
|
965
|
+
}
|
|
966
|
+
async function detectInstallCommand(cwd) {
|
|
967
|
+
if (await pathExists(path.join(cwd, 'pnpm-lock.yaml'))) {
|
|
968
|
+
return { command: 'pnpm', args: ['install'] };
|
|
969
|
+
}
|
|
970
|
+
if (await pathExists(path.join(cwd, 'yarn.lock'))) {
|
|
971
|
+
return { command: 'yarn', args: ['install'] };
|
|
972
|
+
}
|
|
973
|
+
if ((await pathExists(path.join(cwd, 'bun.lock'))) ||
|
|
974
|
+
(await pathExists(path.join(cwd, 'bun.lockb')))) {
|
|
975
|
+
return { command: 'bun', args: ['install'] };
|
|
976
|
+
}
|
|
977
|
+
return { command: 'npm', args: ['install'] };
|
|
978
|
+
}
|
|
979
|
+
async function runCommand(command, args, cwd) {
|
|
980
|
+
await new Promise((resolve, reject) => {
|
|
981
|
+
const child = spawn(command, args, {
|
|
982
|
+
cwd,
|
|
983
|
+
shell: process.platform === 'win32',
|
|
984
|
+
stdio: 'inherit',
|
|
985
|
+
});
|
|
986
|
+
child.on('exit', (code) => {
|
|
987
|
+
if (code === 0) {
|
|
988
|
+
resolve();
|
|
989
|
+
return;
|
|
990
|
+
}
|
|
991
|
+
reject(new Error(`${command} ${args.join(' ')} failed with exit code ${code}.`));
|
|
992
|
+
});
|
|
993
|
+
child.on('error', reject);
|
|
994
|
+
});
|
|
995
|
+
}
|
|
996
|
+
async function initProject(cwd, args) {
|
|
997
|
+
const force = hasFlag(args, '--force');
|
|
998
|
+
const answers = await collectInitAnswers(cwd, args);
|
|
999
|
+
const config = await makeConfig(cwd, answers);
|
|
1000
|
+
const created = [];
|
|
1001
|
+
const skipped = [];
|
|
1002
|
+
await writeGeneratedFile({
|
|
1003
|
+
content: configSource(config),
|
|
1004
|
+
created,
|
|
1005
|
+
filePath: answers.configPath,
|
|
1006
|
+
force,
|
|
1007
|
+
skipped,
|
|
1008
|
+
});
|
|
1009
|
+
await writeNextAdapterFiles({
|
|
1010
|
+
answers,
|
|
1011
|
+
configPath: answers.configPath,
|
|
1012
|
+
created,
|
|
1013
|
+
cwd,
|
|
1014
|
+
force,
|
|
1015
|
+
skipped,
|
|
1016
|
+
});
|
|
1017
|
+
await updateTailwindSource({
|
|
1018
|
+
appDir: answers.appDir,
|
|
1019
|
+
created,
|
|
1020
|
+
cwd,
|
|
1021
|
+
skipped,
|
|
1022
|
+
});
|
|
1023
|
+
await updatePackageJson({ created, cwd, force, skipped });
|
|
1024
|
+
if (hasFlag(args, '--install')) {
|
|
1025
|
+
const install = await detectInstallCommand(cwd);
|
|
1026
|
+
await runCommand(install.command, install.args, cwd);
|
|
1027
|
+
}
|
|
1028
|
+
if (answers.shouldBuild) {
|
|
1029
|
+
const configForBuild = skipped.includes(answers.configPath) && !force
|
|
1030
|
+
? await loadConfig(answers.configPath)
|
|
1031
|
+
: config;
|
|
1032
|
+
await buildBrandKit(configForBuild, { cwd });
|
|
1033
|
+
}
|
|
1034
|
+
return {
|
|
1035
|
+
config,
|
|
1036
|
+
configPath: answers.configPath,
|
|
1037
|
+
created,
|
|
1038
|
+
skipped,
|
|
1039
|
+
};
|
|
1040
|
+
}
|
|
1041
|
+
async function main() {
|
|
1042
|
+
const [, , command = 'help', ...args] = process.argv;
|
|
1043
|
+
const cwd = process.cwd();
|
|
1044
|
+
if (command === 'help' || command === '--help' || command === '-h') {
|
|
1045
|
+
printHelp();
|
|
1046
|
+
return;
|
|
1047
|
+
}
|
|
1048
|
+
if (command === 'init' || command === 'install') {
|
|
1049
|
+
const result = await initProject(cwd, args);
|
|
1050
|
+
for (const filePath of result.created) {
|
|
1051
|
+
console.log(`Created ${path.relative(cwd, filePath)}`);
|
|
1052
|
+
}
|
|
1053
|
+
for (const filePath of result.skipped) {
|
|
1054
|
+
console.log(`Skipped existing ${path.relative(cwd, filePath)}`);
|
|
1055
|
+
}
|
|
1056
|
+
console.log(`Brand Kit route: ${result.config.route ?? '/brandkit'}`);
|
|
1057
|
+
console.log('Run npm run brandkit:build after changing logos or colors.');
|
|
1058
|
+
return;
|
|
1059
|
+
}
|
|
1060
|
+
if (command === 'build') {
|
|
1061
|
+
const ownPackage = await readOwnPackageJson();
|
|
1062
|
+
const configPath = await findConfigPath(cwd, getFlagValue(args, '--config'));
|
|
1063
|
+
const config = await loadConfig(configPath);
|
|
1064
|
+
const result = await buildBrandKit(config, { cwd });
|
|
1065
|
+
console.log(`Built with ${ownPackage.name ?? 'open-brandkit'}@${ownPackage.version ?? 'latest'}`);
|
|
1066
|
+
console.log(`Generated ${path.relative(cwd, result.sitePath)}`);
|
|
1067
|
+
console.log(`Generated ${path.relative(cwd, result.manifestPath)}`);
|
|
1068
|
+
console.log(`Wrote ${result.writtenFiles.length} files`);
|
|
1069
|
+
return;
|
|
1070
|
+
}
|
|
1071
|
+
printHelp();
|
|
1072
|
+
throw new Error(`Unknown command: ${command}`);
|
|
1073
|
+
}
|
|
1074
|
+
main().catch((error) => {
|
|
1075
|
+
const message = error instanceof Error ? error.message : 'Open BrandKit failed.';
|
|
1076
|
+
console.error(message);
|
|
1077
|
+
process.exitCode = 1;
|
|
1078
|
+
});
|
|
1079
|
+
//# sourceMappingURL=index.js.map
|