create-okjt-app 0.1.0 → 0.1.1

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/src/create.mjs CHANGED
@@ -1,132 +1,132 @@
1
- /**
2
- * Writes the project and installs it: the React app from templates, the
3
- * Laravel API from `laravel/laravel` with the kit on top, then checks both
4
- * (the API's tests, the app's build).
5
- */
6
- import { spawnSync } from 'node:child_process';
7
- import { randomBytes } from 'node:crypto';
8
- import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';
9
- import { dirname, join } from 'node:path';
10
- import { backendFiles } from './templates/backend.mjs';
11
- import { frontendFiles } from './templates/frontend.mjs';
12
- import { rootFiles } from './templates/root.mjs';
13
-
14
- export function writeFiles(dir, files) {
15
- for (const [path, body] of Object.entries(files)) {
16
- const target = join(dir, path);
17
- mkdirSync(dirname(target), { recursive: true });
18
- writeFileSync(target, body);
19
- }
20
- }
21
-
22
- function run(command, args, cwd, log) {
23
- log(` $ ${command} ${args.join(' ')}`);
24
- const result = spawnSync(command, args, { cwd, stdio: 'inherit', shell: process.platform === 'win32' });
25
- if (result.error) throw new Error(`${command} isn't available (${result.error.code}). Install it and try again.`);
26
- if (result.status !== 0) throw new Error(`"${command} ${args.join(' ')}" failed in ${cwd}.`);
27
- }
28
-
29
- /** Sets keys in an .env file: replaces the ones it has, adds the rest at the end. */
30
- export function setEnv(text, values) {
31
- let out = text.endsWith('\n') ? text : `${text}\n`;
32
- const added = [];
33
- for (const [key, value] of Object.entries(values)) {
34
- const line = `${key}=${value}`;
35
- const pattern = new RegExp(`^#?\\s*${key}=.*$`, 'm');
36
- if (pattern.test(out)) out = out.replace(pattern, () => line);
37
- else added.push(line);
38
- }
39
- return added.length ? `${out}\n${added.join('\n')}\n` : out;
40
- }
41
-
42
- /** The .env settings the kit and its features read. */
43
- export function envValues(p, { example = false } = {}) {
44
- const quote = (v) => (/\s/.test(v) ? `"${v}"` : v);
45
- return {
46
- APP_NAME: quote(p.name),
47
- APP_URL: 'http://localhost:8000',
48
- FRONTEND_URL: 'http://localhost:5173',
49
- OKJT_ADMIN_NAME: quote(`${p.name} admin`),
50
- OKJT_ADMIN_EMAIL: `admin@${p.domain}`,
51
- OKJT_ADMIN_PASSWORD: example ? '' : randomBytes(9).toString('base64url'),
52
- ...(p.features.backups ? { AUTO_BACKUPS: 'true' } : {}),
53
- ...(p.features.billing ? { BILLING_ENABLED: 'false', BILLING_CURRENCY: 'KES', PAYSTACK_SECRET_KEY: '', PAYSTACK_PUBLIC_KEY: '' } : {}),
54
- ...(p.features.google ? { GOOGLE_CLIENT_ID: '', GOOGLE_CLIENT_SECRET: '', GOOGLE_REDIRECT_URI: '"${APP_URL}/api/auth/google/callback"' } : {}),
55
- };
56
- }
57
-
58
- /** Laravel's own files the kit needs a line in (kept, not replaced). */
59
- export function patchLaravel(backend, p) {
60
- const appConfig = join(backend, 'config/app.php');
61
- const app = readFileSync(appConfig, 'utf8');
62
- if (!app.includes("'frontend_url'")) {
63
- writeFileSync(appConfig, app.replace(/(\n\s*'url' => env\('APP_URL'[^\n]*\n)/, (m) => `${m}\n // Where the React app is (links in emails, Google sign-in's return).\n 'frontend_url' => env('FRONTEND_URL', env('APP_URL')),\n`));
64
- }
65
- if (p.features.google) {
66
- const servicesPath = join(backend, 'config/services.php');
67
- const services = readFileSync(servicesPath, 'utf8');
68
- if (!services.includes("'google'")) {
69
- writeFileSync(servicesPath, services.replace(/\n\];\s*$/, "\n\n 'google' => [\n 'client_id' => env('GOOGLE_CLIENT_ID'),\n 'client_secret' => env('GOOGLE_CLIENT_SECRET'),\n 'redirect' => env('GOOGLE_REDIRECT_URI'),\n ],\n\n];\n"));
70
- }
71
- }
72
- for (const [file, example] of [['.env', false], ['.env.example', true]]) {
73
- const path = join(backend, file);
74
- if (existsSync(path)) writeFileSync(path, setEnv(readFileSync(path, 'utf8'), envValues(p, { example })));
75
- }
76
- }
77
-
78
- export async function createProject(p, { dir, install = true, checks = true, git = true, log = console.log }) {
79
- if (existsSync(dir) && readdirSync(dir).length) throw new Error(`${dir} already exists and isn't empty.`);
80
- mkdirSync(dir, { recursive: true });
81
- const backend = join(dir, 'backend');
82
- const frontend = join(dir, 'frontend');
83
-
84
- log(`\nWriting ${p.name} in ${dir}`);
85
- writeFiles(dir, rootFiles(p));
86
- writeFiles(frontend, frontendFiles(p));
87
-
88
- if (!install) {
89
- writeFiles(backend, backendFiles(p));
90
- log("Files written (--no-install: backend/ holds only the kit's files, without Laravel).");
91
- return;
92
- }
93
-
94
- log('\nThe API: Laravel 12 with the OKJT kit');
95
- run('composer', ['create-project', 'laravel/laravel:^12.0', 'backend', '--no-interaction', '--prefer-dist', '--quiet'], dir, log);
96
- run('php', ['artisan', 'install:api', '--without-migration-prompt', '--no-interaction'], backend, log);
97
- const packages = ['okjt/laravel-kit:^0.2', 'bacon/bacon-qr-code:^3.0'];
98
- if (p.features.google) packages.push('laravel/socialite:^5.0');
99
- if (p.features.passkeys) packages.push('laravel/passkeys:^0.2');
100
- run('composer', ['require', ...packages, '--no-interaction', '--quiet'], backend, log);
101
- writeFiles(backend, backendFiles(p));
102
- patchLaravel(backend, p);
103
- run('php', ['artisan', 'vendor:publish', '--tag=okjt-migrations', '--no-interaction'], backend, log);
104
- if (p.features.passkeys) run('php', ['artisan', 'vendor:publish', '--tag=passkeys-migrations', '--no-interaction'], backend, log);
105
- run('php', ['artisan', 'migrate', '--force', '--no-interaction'], backend, log);
106
- run('php', ['artisan', 'db:seed', '--force', '--no-interaction'], backend, log);
107
-
108
- log('\nThe app: React on @okjt/site and @okjt/dashboard');
109
- run('npm', ['install', '--no-audit', '--no-fund'], frontend, log);
110
-
111
- if (checks) {
112
- log('\nChecking it works');
113
- run('php', ['artisan', 'test'], backend, log);
114
- run('npm', ['run', 'build'], frontend, log);
115
- }
116
-
117
- if (git && !existsSync(join(dir, '.git')) && spawnSync('git', ['init', '-q', '-b', 'main'], { cwd: dir }).status === 0) {
118
- spawnSync('git', ['add', '-A'], { cwd: dir });
119
- spawnSync('git', ['commit', '-q', '-m', `${p.name}: created with create-okjt-app`], { cwd: dir });
120
- }
121
-
122
- const env = readFileSync(join(backend, '.env'), 'utf8');
123
- log(`
124
- ${p.name} is ready.
125
-
126
- cd ${dir}/backend && php artisan serve
127
- cd ${dir}/frontend && npm run dev → http://localhost:5173
128
-
129
- Sign in as ${env.match(/^OKJT_ADMIN_EMAIL=(.*)$/m)?.[1]} with the password ${env.match(/^OKJT_ADMIN_PASSWORD=(.*)$/m)?.[1]} (in backend/.env).
130
- Deploying: README.md › Deploy.
131
- `);
132
- }
1
+ /**
2
+ * Writes the project and installs it: the React app from templates, the
3
+ * Laravel API from `laravel/laravel` with the kit on top, then checks both
4
+ * (the API's tests, the app's build).
5
+ */
6
+ import { spawnSync } from 'node:child_process';
7
+ import { randomBytes } from 'node:crypto';
8
+ import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';
9
+ import { dirname, join } from 'node:path';
10
+ import { backendFiles } from './templates/backend.mjs';
11
+ import { frontendFiles } from './templates/frontend.mjs';
12
+ import { rootFiles } from './templates/root.mjs';
13
+
14
+ export function writeFiles(dir, files) {
15
+ for (const [path, body] of Object.entries(files)) {
16
+ const target = join(dir, path);
17
+ mkdirSync(dirname(target), { recursive: true });
18
+ writeFileSync(target, body);
19
+ }
20
+ }
21
+
22
+ function run(command, args, cwd, log) {
23
+ log(` $ ${command} ${args.join(' ')}`);
24
+ const result = spawnSync(command, args, { cwd, stdio: 'inherit', shell: process.platform === 'win32' });
25
+ if (result.error) throw new Error(`${command} isn't available (${result.error.code}). Install it and try again.`);
26
+ if (result.status !== 0) throw new Error(`"${command} ${args.join(' ')}" failed in ${cwd}.`);
27
+ }
28
+
29
+ /** Sets keys in an .env file: replaces the ones it has, adds the rest at the end. */
30
+ export function setEnv(text, values) {
31
+ let out = text.endsWith('\n') ? text : `${text}\n`;
32
+ const added = [];
33
+ for (const [key, value] of Object.entries(values)) {
34
+ const line = `${key}=${value}`;
35
+ const pattern = new RegExp(`^#?\\s*${key}=.*$`, 'm');
36
+ if (pattern.test(out)) out = out.replace(pattern, () => line);
37
+ else added.push(line);
38
+ }
39
+ return added.length ? `${out}\n${added.join('\n')}\n` : out;
40
+ }
41
+
42
+ /** The .env settings the kit and its features read. */
43
+ export function envValues(p, { example = false } = {}) {
44
+ const quote = (v) => (/\s/.test(v) ? `"${v}"` : v);
45
+ return {
46
+ APP_NAME: quote(p.name),
47
+ APP_URL: 'http://localhost:8000',
48
+ FRONTEND_URL: 'http://localhost:5173',
49
+ OKJT_ADMIN_NAME: quote(`${p.name} admin`),
50
+ OKJT_ADMIN_EMAIL: `admin@${p.domain}`,
51
+ OKJT_ADMIN_PASSWORD: example ? '' : randomBytes(9).toString('base64url'),
52
+ ...(p.features.backups ? { AUTO_BACKUPS: 'true' } : {}),
53
+ ...(p.features.billing ? { BILLING_ENABLED: 'false', BILLING_CURRENCY: 'KES', PAYSTACK_SECRET_KEY: '', PAYSTACK_PUBLIC_KEY: '' } : {}),
54
+ ...(p.features.google ? { GOOGLE_CLIENT_ID: '', GOOGLE_CLIENT_SECRET: '', GOOGLE_REDIRECT_URI: '"${APP_URL}/api/auth/google/callback"' } : {}),
55
+ };
56
+ }
57
+
58
+ /** Laravel's own files the kit needs a line in (kept, not replaced). */
59
+ export function patchLaravel(backend, p) {
60
+ const appConfig = join(backend, 'config/app.php');
61
+ const app = readFileSync(appConfig, 'utf8');
62
+ if (!app.includes("'frontend_url'")) {
63
+ writeFileSync(appConfig, app.replace(/(\n\s*'url' => env\('APP_URL'[^\n]*\n)/, (m) => `${m}\n // Where the React app is (links in emails, Google sign-in's return).\n 'frontend_url' => env('FRONTEND_URL', env('APP_URL')),\n`));
64
+ }
65
+ if (p.features.google) {
66
+ const servicesPath = join(backend, 'config/services.php');
67
+ const services = readFileSync(servicesPath, 'utf8');
68
+ if (!services.includes("'google'")) {
69
+ writeFileSync(servicesPath, services.replace(/\n\];\s*$/, "\n\n 'google' => [\n 'client_id' => env('GOOGLE_CLIENT_ID'),\n 'client_secret' => env('GOOGLE_CLIENT_SECRET'),\n 'redirect' => env('GOOGLE_REDIRECT_URI'),\n ],\n\n];\n"));
70
+ }
71
+ }
72
+ for (const [file, example] of [['.env', false], ['.env.example', true]]) {
73
+ const path = join(backend, file);
74
+ if (existsSync(path)) writeFileSync(path, setEnv(readFileSync(path, 'utf8'), envValues(p, { example })));
75
+ }
76
+ }
77
+
78
+ export async function createProject(p, { dir, install = true, checks = true, git = true, log = console.log }) {
79
+ if (existsSync(dir) && readdirSync(dir).length) throw new Error(`${dir} already exists and isn't empty.`);
80
+ mkdirSync(dir, { recursive: true });
81
+ const backend = join(dir, 'backend');
82
+ const frontend = join(dir, 'frontend');
83
+
84
+ log(`\nWriting ${p.name} in ${dir}`);
85
+ writeFiles(dir, rootFiles(p));
86
+ writeFiles(frontend, frontendFiles(p));
87
+
88
+ if (!install) {
89
+ writeFiles(backend, backendFiles(p));
90
+ log("Files written (--no-install: backend/ holds only the kit's files, without Laravel).");
91
+ return;
92
+ }
93
+
94
+ log('\nThe API: Laravel 12 with the OKJT kit');
95
+ run('composer', ['create-project', 'laravel/laravel:^12.0', 'backend', '--no-interaction', '--prefer-dist', '--quiet'], dir, log);
96
+ run('php', ['artisan', 'install:api', '--without-migration-prompt', '--no-interaction'], backend, log);
97
+ const packages = ['okjt/laravel-kit:^0.2.2', 'bacon/bacon-qr-code:^3.0'];
98
+ if (p.features.google) packages.push('laravel/socialite:^5.0');
99
+ if (p.features.passkeys) packages.push('laravel/passkeys:^0.2');
100
+ run('composer', ['require', ...packages, '--no-interaction', '--quiet'], backend, log);
101
+ writeFiles(backend, backendFiles(p));
102
+ patchLaravel(backend, p);
103
+ run('php', ['artisan', 'vendor:publish', '--tag=okjt-migrations', '--no-interaction'], backend, log);
104
+ if (p.features.passkeys) run('php', ['artisan', 'vendor:publish', '--tag=passkeys-migrations', '--no-interaction'], backend, log);
105
+ run('php', ['artisan', 'migrate', '--force', '--no-interaction'], backend, log);
106
+ run('php', ['artisan', 'db:seed', '--force', '--no-interaction'], backend, log);
107
+
108
+ log('\nThe app: React on @okjt/site and @okjt/dashboard');
109
+ run('npm', ['install', '--no-audit', '--no-fund'], frontend, log);
110
+
111
+ if (checks) {
112
+ log('\nChecking it works');
113
+ run('php', ['artisan', 'test'], backend, log);
114
+ run('npm', ['run', 'build'], frontend, log);
115
+ }
116
+
117
+ if (git && !existsSync(join(dir, '.git')) && spawnSync('git', ['init', '-q', '-b', 'main'], { cwd: dir }).status === 0) {
118
+ spawnSync('git', ['add', '-A'], { cwd: dir });
119
+ spawnSync('git', ['commit', '-q', '-m', `${p.name}: created with create-okjt-app`], { cwd: dir });
120
+ }
121
+
122
+ const env = readFileSync(join(backend, '.env'), 'utf8');
123
+ log(`
124
+ ${p.name} is ready.
125
+
126
+ cd ${dir}/backend && php artisan serve
127
+ cd ${dir}/frontend && npm run dev → http://localhost:5173
128
+
129
+ Sign in as ${env.match(/^OKJT_ADMIN_EMAIL=(.*)$/m)?.[1]} with the password ${env.match(/^OKJT_ADMIN_PASSWORD=(.*)$/m)?.[1]} (in backend/.env).
130
+ Deploying: README.md › Deploy.
131
+ `);
132
+ }
package/src/palette.mjs CHANGED
@@ -1,42 +1,42 @@
1
- /**
2
- * The theme's seventeen colours (@okjt/ui's tokens) from the two a project
3
- * chooses: its accent and its ink (the darkest text). Lighter and darker
4
- * shades are mixes towards white and black, so the palette stays in one
5
- * family; edit okjt.config.js to fine-tune any of them.
6
- */
7
-
8
- const toRgb = (hex) => [1, 3, 5].map((i) => parseInt(hex.slice(i, i + 2), 16));
9
- const toHex = (rgb) => `#${rgb.map((v) => Math.round(Math.min(255, Math.max(0, v))).toString(16).padStart(2, '0')).join('')}`.toUpperCase();
10
-
11
- /** `amount` of the way from `hex` to `towards` (0 = hex, 1 = towards). */
12
- export function mix(hex, towards, amount) {
13
- const a = toRgb(hex);
14
- const b = toRgb(towards);
15
- return toHex(a.map((v, i) => v + (b[i] - v) * amount));
16
- }
17
-
18
- const WHITE = '#FFFFFF';
19
- const BLACK = '#000000';
20
-
21
- export function palette({ accent, ink, tone = 'stone' }) {
22
- return {
23
- ink,
24
- inkDeep: mix(ink, BLACK, 0.3),
25
- body: mix(ink, WHITE, 0.15),
26
- soft: mix(ink, WHITE, 0.3),
27
- muted: mix(ink, WHITE, 0.45),
28
- faint: mix(ink, WHITE, 0.58),
29
- accent,
30
- accentStrong: mix(accent, BLACK, 0.1),
31
- accentDark: mix(accent, BLACK, 0.25),
32
- accentSoft: mix(accent, WHITE, 0.4),
33
- line: mix(accent, WHITE, 0.85),
34
- lineStrong: mix(accent, WHITE, 0.75),
35
- tint: mix(accent, WHITE, 0.93),
36
- paper: mix(accent, WHITE, 0.975),
37
- paperWarm: mix(accent, WHITE, 0.96),
38
- positive: '#5B8A5B',
39
- positiveDark: '#3F6B3F',
40
- tone,
41
- };
42
- }
1
+ /**
2
+ * The theme's seventeen colours (@okjt/ui's tokens) from the two a project
3
+ * chooses: its accent and its ink (the darkest text). Lighter and darker
4
+ * shades are mixes towards white and black, so the palette stays in one
5
+ * family; edit okjt.config.js to fine-tune any of them.
6
+ */
7
+
8
+ const toRgb = (hex) => [1, 3, 5].map((i) => parseInt(hex.slice(i, i + 2), 16));
9
+ const toHex = (rgb) => `#${rgb.map((v) => Math.round(Math.min(255, Math.max(0, v))).toString(16).padStart(2, '0')).join('')}`.toUpperCase();
10
+
11
+ /** `amount` of the way from `hex` to `towards` (0 = hex, 1 = towards). */
12
+ export function mix(hex, towards, amount) {
13
+ const a = toRgb(hex);
14
+ const b = toRgb(towards);
15
+ return toHex(a.map((v, i) => v + (b[i] - v) * amount));
16
+ }
17
+
18
+ const WHITE = '#FFFFFF';
19
+ const BLACK = '#000000';
20
+
21
+ export function palette({ accent, ink, tone = 'stone' }) {
22
+ return {
23
+ ink,
24
+ inkDeep: mix(ink, BLACK, 0.3),
25
+ body: mix(ink, WHITE, 0.15),
26
+ soft: mix(ink, WHITE, 0.3),
27
+ muted: mix(ink, WHITE, 0.45),
28
+ faint: mix(ink, WHITE, 0.58),
29
+ accent,
30
+ accentStrong: mix(accent, BLACK, 0.1),
31
+ accentDark: mix(accent, BLACK, 0.25),
32
+ accentSoft: mix(accent, WHITE, 0.4),
33
+ line: mix(accent, WHITE, 0.85),
34
+ lineStrong: mix(accent, WHITE, 0.75),
35
+ tint: mix(accent, WHITE, 0.93),
36
+ paper: mix(accent, WHITE, 0.975),
37
+ paperWarm: mix(accent, WHITE, 0.96),
38
+ positive: '#5B8A5B',
39
+ positiveDark: '#3F6B3F',
40
+ tone,
41
+ };
42
+ }