adminforth 1.19.0 → 1.20.1-next.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.
Files changed (30) hide show
  1. package/commands/bundle.js +15 -21
  2. package/commands/callTsProxy.js +121 -0
  3. package/commands/cli.js +52 -2
  4. package/commands/createApp/templates/.dockerignore.hbs +4 -0
  5. package/commands/createApp/templates/.env.prod.hbs +6 -0
  6. package/commands/createApp/templates/Dockerfile.hbs +7 -0
  7. package/commands/createApp/templates/index.ts.hbs +2 -2
  8. package/commands/createApp/templates/package.json.hbs +8 -5
  9. package/commands/createApp/templates/readme.md.hbs +14 -2
  10. package/commands/createApp/utils.js +38 -13
  11. package/commands/createCustomComponent/configLoader.js +57 -0
  12. package/commands/createCustomComponent/configUpdater.js +207 -0
  13. package/commands/createCustomComponent/fileGenerator.js +72 -0
  14. package/commands/createCustomComponent/main.js +138 -0
  15. package/commands/createCustomComponent/templates/customFields/create.vue.hbs +41 -0
  16. package/commands/createCustomComponent/templates/customFields/edit.vue.hbs +41 -0
  17. package/commands/createCustomComponent/templates/customFields/list.vue.hbs +18 -0
  18. package/commands/createCustomComponent/templates/customFields/show.vue.hbs +17 -0
  19. package/commands/proxy.ts +60 -0
  20. package/dist/commands/proxy.d.ts +2 -0
  21. package/dist/commands/proxy.d.ts.map +1 -0
  22. package/dist/commands/proxy.js +71 -0
  23. package/dist/commands/proxy.js.map +1 -0
  24. package/dist/index.js +1 -1
  25. package/dist/index.js.map +1 -1
  26. package/dist/modules/codeInjector.d.ts +2 -0
  27. package/dist/modules/codeInjector.d.ts.map +1 -1
  28. package/dist/modules/codeInjector.js +64 -13
  29. package/dist/modules/codeInjector.js.map +1 -1
  30. package/package.json +5 -1
@@ -1,28 +1,22 @@
1
- import fs from "fs";
2
- import { getInstance } from "./utils.js";
1
+ import { callTsProxy, findAdminInstance } from "./callTsProxy.js";
2
+
3
3
 
4
4
  async function bundle() {
5
- const currentDirectory = process.cwd();
6
- const files = fs.readdirSync(currentDirectory);
7
- let instanceFound = false;
5
+ console.log("Bundling admin SPA...");
6
+ const instance = await findAdminInstance();
7
+
8
+
9
+ try {
10
+ await callTsProxy(`
11
+ import { admin } from './${instance.file}.js';
8
12
 
9
- for (const file of files) {
10
- if (file.endsWith(".js") || file.endsWith(".ts")) {
11
- try {
12
- const instance = await getInstance(file, currentDirectory);
13
- if (instance) {
14
- await instance.bundleNow({ hotReload: false });
15
- instanceFound = true;
16
- break;
17
- }
18
- } catch (error) {
19
- console.error(`Error: Could not bundle '${file}'`, error);
13
+ export async function exec() {
14
+ return await admin.bundleNow({ hotReload: false });
20
15
  }
21
- }
22
- }
23
- if (!instanceFound) {
24
- console.error("Error: No valid instance found to bundle.");
25
- return;
16
+ `);
17
+
18
+ } catch (e) {
19
+ console.log(`Running file ${file} failed`, e);
26
20
  }
27
21
  }
28
22
 
@@ -0,0 +1,121 @@
1
+ // callTsProxy.js
2
+ import { spawn } from "child_process";
3
+ import path from "path";
4
+ import fs from "fs";
5
+ import chalk from "chalk";
6
+
7
+ const currentFilePath = import.meta.url;
8
+ const currentFileFolder = path.dirname(currentFilePath).replace("file:", "");
9
+
10
+ export function callTsProxy(tsCode) {
11
+
12
+ process.env.HEAVY_DEBUG && console.log("🌐 Calling tsproxy with code:", path.join(currentFileFolder, "proxy.ts"));
13
+ return new Promise((resolve, reject) => {
14
+ const child = spawn("tsx", [
15
+ path.join(currentFileFolder, "proxy.ts")
16
+ ]);
17
+
18
+ let stdout = "";
19
+ let stderr = "";
20
+
21
+ child.stdout.on("data", (data) => {
22
+ stdout += data;
23
+ });
24
+
25
+ child.stderr.on("data", (data) => {
26
+ stderr += data;
27
+ });
28
+
29
+ child.on("close", (code) => {
30
+ if (code === 0) {
31
+ try {
32
+ const parsed = JSON.parse(stdout);
33
+ parsed.capturedLogs.forEach((log) => {
34
+ console.log(...log);
35
+ });
36
+
37
+ if (parsed.error) {
38
+ reject(new Error(`${parsed.error}\n${parsed.stack}`));
39
+ }
40
+ resolve(parsed.result);
41
+ } catch (e) {
42
+ reject(new Error("Invalid JSON from tsproxy: " + stdout));
43
+ }
44
+ } else {
45
+ console.error(`tsproxy exited with non-0, this should never happen, stdout: ${stdout}, stderr: ${stderr}`);
46
+ reject(new Error(stderr));
47
+ }
48
+ });
49
+
50
+ process.env.HEAVY_DEBUG && console.log("🪲 Writing to tsproxy stdin...\n'''", tsCode, "'''");
51
+ child.stdin.write(tsCode);
52
+ child.stdin.end();
53
+ });
54
+ }
55
+
56
+ export async function findAdminInstance() {
57
+ process.env.HEAVY_DEBUG && console.log("🌐 Finding admin instance...");
58
+ const currentDirectory = process.cwd();
59
+
60
+ let files = fs.readdirSync(currentDirectory);
61
+ let instanceFound = {
62
+ file: null,
63
+ version: null,
64
+ };
65
+ // try index.ts first
66
+ if (files.includes("index.ts")) {
67
+ files = files.filter((file) => file !== "index.ts");
68
+ files.unshift("index.ts");
69
+ }
70
+
71
+ for (const file of files) {
72
+ if (file.endsWith(".ts")) {
73
+ const fileNoTs = file.replace(/\.ts$/, "");
74
+ process.env.HEAVY_DEBUG && console.log(`🪲 Trying bundleing ${file}...`);
75
+ try {
76
+ res = await callTsProxy(`
77
+ import { admin } from './${fileNoTs}.js';
78
+
79
+ export async function exec() {
80
+ return admin.formatAdminForth();
81
+ }
82
+ `);
83
+ instanceFound.file = fileNoTs;
84
+ instanceFound.version = res;
85
+ break;
86
+
87
+ } catch (e) {
88
+ // do our best to guess that this file has a good chance to be admin instance
89
+ // and show the error so user can fix it
90
+ const fileContent = fs.readFileSync(file, "utf-8");
91
+ if (fileContent.includes("export const admin")) {
92
+ console.error(chalk.red(`Error running ${file}:`, e));
93
+ process.exit(1);
94
+ }
95
+ process.env.HEAVY_DEBUG && console.log(`🪲 File ${file} failed`, e);
96
+ }
97
+ }
98
+ }
99
+ if (!instanceFound.file) {
100
+ console.error(
101
+ chalk.red(
102
+ `Error: No valid instance found to bundle.\n` +
103
+ `Make sure you have a file in the current directory with a .ts extension, and it exports an ` +
104
+ chalk.cyan.bold('admin') +
105
+ ` instance like:\n\n` +
106
+ chalk.yellow('export const admin = new AdminForth({...})') +
107
+ `\n\nFor example, adminforth CLI creates an index.ts file which exports the admin instance.`
108
+ )
109
+ );
110
+ process.exit(1);
111
+ }
112
+ return instanceFound;
113
+ }
114
+
115
+ // Example usage:
116
+ // callTsProxy(`
117
+ // import admin from './admin';
118
+ // function exec() {
119
+ // return admin.doX();
120
+ // }
121
+ // `).then(console.log).catch(console.error);
package/commands/cli.js CHANGED
@@ -7,6 +7,41 @@ import bundle from "./bundle.js";
7
7
  import createApp from "./createApp/main.js";
8
8
  import generateModels from "./generateModels.js";
9
9
  import createPlugin from "./createPlugin/main.js";
10
+ import createComponent from "./createCustomComponent/main.js";
11
+ import chalk from "chalk";
12
+ import path from "path";
13
+ import fs from "fs";
14
+
15
+ function showHelp() {
16
+ console.log(
17
+ chalk.white("Available commands:\n") +
18
+ chalk.green(' create-app') + chalk.white(' Create a new AdminForth app\n') +
19
+ chalk.green(' create-plugin') + chalk.white(' Create a plugin for your AdminForth app\n') +
20
+ chalk.green(' generate-models') + chalk.white(' Generate TypeScript models from your databases\n') +
21
+ chalk.green(' bundle') + chalk.white(' Bundles your AdminForth app SPA for production\n') +
22
+ chalk.green(' component') + chalk.white(' Scaffold a custom Vue component\n')
23
+ );
24
+ }
25
+
26
+ function currentFileDir(importMetaUrl) {
27
+ const filePath = importMetaUrl.replace("file://", "");
28
+ const fileDir = path.dirname(filePath);
29
+ return fileDir;
30
+ }
31
+
32
+ function showVersion() {
33
+ const ADMIN_FORTH_ABSOLUTE_PATH = path.join(currentFileDir(import.meta.url), '..');
34
+
35
+ const package_json = JSON.parse(fs.readFileSync(path.join(ADMIN_FORTH_ABSOLUTE_PATH, 'package.json'), 'utf8'));
36
+
37
+ const ADMINFORTH_VERSION = package_json.version;
38
+
39
+ console.log(
40
+ chalk.white('AdminForth CLI version: ') +
41
+ chalk.cyan.bold(ADMINFORTH_VERSION)
42
+ );
43
+ }
44
+
10
45
  switch (command) {
11
46
  case "create-app":
12
47
  createApp(args);
@@ -20,8 +55,23 @@ switch (command) {
20
55
  case "bundle":
21
56
  bundle();
22
57
  break;
23
- default:
58
+ case "component":
59
+ createComponent(args);
60
+ break;
61
+ case "help":
62
+ case "--help":
63
+ case "-h":
64
+ showHelp();
65
+ break;
66
+ case "--version":
67
+ case "version":
68
+ case "-v":
69
+ showVersion();
70
+ break;
71
+ default: {
24
72
  console.log(
25
- "Unknown command. Available commands: create-app, create-plugin, generate-models, bundle"
73
+ "Unknown command."
26
74
  );
75
+ showHelp();
76
+ }
27
77
  }
@@ -0,0 +1,4 @@
1
+ node_modules
2
+ {{#if sqliteFile}}
3
+ {{ sqliteFile }}
4
+ {{/if}}
@@ -0,0 +1,6 @@
1
+ NODE_ENV=production
2
+ DATABASE_URL=sqlite://.db.sqlite
3
+ DATABASE_URL={{dbUrl}}
4
+ {{#if prismaDbUrl}}
5
+ PRISMA_DATABASE_URL={{prismaDbUrl}}
6
+ {{/if}}
@@ -0,0 +1,7 @@
1
+ FROM node:{{nodeMajor}}-slim
2
+ WORKDIR /code/
3
+ ADD package.json package-lock.json /code/
4
+ RUN npm ci
5
+ ADD . /code/
6
+ RUN --mount=type=cache,target=/tmp npx adminforth bundle
7
+ CMD ["sh", "-c", "npm run migrate:prod && npm run prod"]
@@ -60,9 +60,9 @@ if (import.meta.url === `file://${process.argv[1]}`) {
60
60
  app.use(express.json());
61
61
 
62
62
  const port = 3500;
63
-
63
+
64
64
  await admin.bundleNow({ hotReload: process.env.NODE_ENV === 'development' });
65
- console.log('Bundling AdminForth done. For faster serving consider calling bundleNow() from a build script.');
65
+ console.log('Bundling AdminForth SPA done.');
66
66
 
67
67
  admin.express.serve(app)
68
68
 
@@ -8,11 +8,14 @@
8
8
  "license": "ISC",
9
9
  "description": "",
10
10
  "scripts": {
11
- "env": "dotenvx run -f .env.local -f .env --overload --",
12
- "start": "npm run env -- tsx watch index.ts",
13
- "migrateLocal": "npm run env -- npx prisma migrate deploy",
14
- "makemigration": "npm run migrateLocal; npm run env -- npx --yes prisma migrate dev",
15
- "test": "echo \"Error: no test specified\" && exit 1"
11
+ "dev": "npm run _env:dev -- tsx watch index.ts",
12
+ "prod": "npm run _env:prod -- tsx index.ts",
13
+ "start": "npm run dev",
14
+ "makemigration": "npm run _env:dev -- npx --yes prisma migrate dev --create-only",
15
+ "migrate:local": "npm run _env:dev -- npx --yes prisma migrate deploy",
16
+ "migrate:prod": "npm run _env:prod -- npx --yes prisma migrate deploy",
17
+ "_env:dev": "dotenvx run -f .env -f .env.local --",
18
+ "_env:prod": "dotenvx run -f .env.prod --"
16
19
  },
17
20
  "engines": {
18
21
  "node": ">=20"
@@ -9,13 +9,13 @@ npm ci
9
9
  Migrate the database:
10
10
 
11
11
  ```bash
12
- npm run migrateLocal
12
+ npm run migrate:local
13
13
  ```
14
14
 
15
15
  Start the server:
16
16
 
17
17
  ```bash
18
- npm start
18
+ npm run dev
19
19
  ```
20
20
 
21
21
  {{#if prismaDbUrl}}
@@ -32,6 +32,18 @@ npm run makemigration -- --name <name_of_changes>
32
32
  Your colleagues will need to pull the changes and run `npm run migrateLocal` to apply the migration in their local database.
33
33
  {{/if}}
34
34
 
35
+ ## Deployment tips
36
+
37
+ You have Dockerfile ready for production deployment. You can test the build with:
38
+
39
+ ```bash
40
+ docker build -t {{appName}}-image .
41
+ docker run -p 3500:3500 {{appName}}-image
42
+ ```
43
+
44
+ To set non-sensitive environment variables in production, use `.env.prod` file.
45
+ For sensitive variables, use direct docker environment variables or secrets from your vault.
46
+
35
47
  ## Documentation
36
48
 
37
49
  - [Customizing AdminForth Branding](https://adminforth.dev/docs/tutorial/Customization/branding/)
@@ -7,8 +7,12 @@ import path from 'path';
7
7
  import { Listr } from 'listr2'
8
8
  import { fileURLToPath } from 'url';
9
9
  import {ConnectionString} from 'connection-string';
10
- import {execa} from 'execa';
10
+ import { exec } from 'child_process';
11
+
11
12
  import Handlebars from 'handlebars';
13
+ import { promisify } from 'util';
14
+
15
+ const execAsync = promisify(exec);
12
16
 
13
17
  export function parseArgumentsIntoOptions(rawArgs) {
14
18
  const args = arg(
@@ -145,18 +149,19 @@ async function scaffoldProject(ctx, options, cwd) {
145
149
  await fse.copy(sourceAssetsDir, targetAssetsDir);
146
150
 
147
151
  // Write templated files
148
- writeTemplateFiles(dirname, projectDir, {
152
+ await writeTemplateFiles(dirname, projectDir, {
149
153
  dbUrl: connectionString.toString(),
150
154
  prismaDbUrl,
151
155
  appName,
152
156
  provider,
157
+ nodeMajor: parseInt(process.versions.node.split('.')[0], 10),
153
158
  });
154
159
 
155
160
  return projectDir; // Return the new directory path
156
161
  }
157
162
 
158
163
  async function writeTemplateFiles(dirname, cwd, options) {
159
- const { dbUrl, prismaDbUrl, appName, provider } = options;
164
+ const { dbUrl, prismaDbUrl, appName, provider, nodeMajor } = options;
160
165
 
161
166
  // Build a list of files to generate
162
167
  const templateTasks = [
@@ -191,10 +196,15 @@ async function writeTemplateFiles(dirname, cwd, options) {
191
196
  dest: '.env.local',
192
197
  data: { dbUrl, prismaDbUrl },
193
198
  },
199
+ {
200
+ src: '.env.prod.hbs',
201
+ dest: '.env.prod',
202
+ data: { dbUrl, prismaDbUrl },
203
+ },
194
204
  {
195
205
  src: 'readme.md.hbs',
196
206
  dest: 'README.md',
197
- data: { dbUrl, prismaDbUrl },
207
+ data: { dbUrl, prismaDbUrl, appName },
198
208
  },
199
209
  {
200
210
  // We'll write .env using the same content as .env.sample
@@ -218,6 +228,18 @@ async function writeTemplateFiles(dirname, cwd, options) {
218
228
  dest: 'custom/tsconfig.json',
219
229
  data: {},
220
230
  },
231
+ {
232
+ src: 'Dockerfile.hbs',
233
+ dest: 'Dockerfile',
234
+ data: { nodeMajor },
235
+ },
236
+ {
237
+ src: '.dockerignore.hbs',
238
+ dest: '.dockerignore',
239
+ data: {
240
+ sqliteFile: detectDbProvider(options.db).startsWith('sqlite') ? options.db.split('://')[1] : null,
241
+ },
242
+ }
221
243
  ];
222
244
 
223
245
  for (const task of templateTasks) {
@@ -228,22 +250,25 @@ async function writeTemplateFiles(dirname, cwd, options) {
228
250
  // fse.ensureDirSync(path.dirname(destPath));
229
251
 
230
252
  if (task.empty) {
231
- fs.writeFileSync(destPath, '');
253
+ await fs.promises.writeFile(destPath, '');
232
254
  } else {
233
255
  const templatePath = path.join(dirname, 'templates', task.src);
234
256
  const compiled = renderHBSTemplate(templatePath, task.data);
235
- fs.writeFileSync(destPath, compiled);
257
+ await fs.promises.writeFile(destPath, compiled);
236
258
  }
237
259
  }
238
260
  }
239
261
 
240
262
  async function installDependencies(ctx, cwd) {
241
- const customDir = ctx.customDir;
263
+ const nodeBinary = process.execPath; // Path to the Node.js binary running this script
264
+ const npmPath = path.join(path.dirname(nodeBinary), 'npm'); // Path to the npm executable
242
265
 
243
- await Promise.all([
244
- await execa('npm', ['install'], { cwd }),
245
- await execa('npm', ['install'], { cwd: customDir }),
266
+ const customDir = ctx.customDir;
267
+ const res = await Promise.all([
268
+ await execAsync(`${nodeBinary} ${npmPath} install`, { cwd, env: { PATH: process.env.PATH } }),
269
+ await execAsync(`${nodeBinary} ${npmPath} install`, { cwd: customDir, env: { PATH: process.env.PATH } }),
246
270
  ]);
271
+ // console.log(chalk.dim(`Dependencies installed in ${cwd} and ${customDir}: \n${res[0].stdout}${res[1].stdout}`));
247
272
  }
248
273
 
249
274
  function generateFinalInstructions(skipPrismaSetup, options) {
@@ -251,15 +276,15 @@ function generateFinalInstructions(skipPrismaSetup, options) {
251
276
  if (!skipPrismaSetup)
252
277
  instruction += `
253
278
  ${chalk.dim('// Go to the project directory')}
254
- ${chalk.cyan(`$ cd ${options.appName}`)}\n`;
279
+ ${chalk.dim('$')}${chalk.cyan(` cd ${options.appName}`)}\n`;
255
280
 
256
281
  instruction += `
257
282
  ${chalk.dim('// Generate and apply initial migration')}
258
- ${chalk.cyan('$ npm run makemigration -- --name init')}\n`;
283
+ ${chalk.dim('$')}${chalk.cyan(' npm run makemigration -- --name init && npm run migrate:local')}\n`;
259
284
 
260
285
  instruction += `
261
286
  ${chalk.dim('// Start dev server with tsx watch for hot-reloading')}
262
- ${chalk.cyan('$ npm start')}\n
287
+ ${chalk.dim('$')}${chalk.cyan(' npm run dev')}\n
263
288
  `;
264
289
 
265
290
  instruction += '😉 Happy coding!';
@@ -0,0 +1,57 @@
1
+ import fs from 'fs/promises';
2
+ import path from 'path';
3
+ import chalk from 'chalk';
4
+ import jiti from 'jiti';
5
+
6
+
7
+ export async function loadAdminForthConfig() {
8
+ const configFileName = 'index.ts';
9
+ const configPath = path.resolve(process.cwd(), configFileName);
10
+
11
+ try {
12
+ await fs.access(configPath);
13
+ } catch (error) {
14
+ console.error(chalk.red(`\nError: Configuration file not found at ${configPath}`));
15
+ console.error(chalk.yellow(`Please ensure you are running this command from your project's root directory and the '${configFileName}' file exists.`));
16
+ process.exit(1);
17
+ }
18
+
19
+ try {
20
+ const _require = jiti(import.meta.url, {
21
+ interopDefault: true,
22
+ cache: true,
23
+ esmResolve: true,
24
+ });
25
+
26
+ const configModule = _require(configPath);
27
+
28
+ const adminInstance = configModule.admin || configModule.default?.admin;
29
+
30
+
31
+ if (!adminInstance) {
32
+ throw new Error(`Could not find 'admin' export in ${configFileName}. Please ensure your config file exports the AdminForth instance like: 'export const admin = new AdminForth({...});'`);
33
+ }
34
+
35
+ const config = adminInstance.config;
36
+
37
+ if (!config || typeof config !== 'object') {
38
+ throw new Error(`Invalid configuration found in admin instance from ${configFileName}. Expected admin.config to be an object.`);
39
+ }
40
+
41
+ if (!config.resources || !Array.isArray(config.resources)) {
42
+ console.warn(chalk.yellow(`Warning: The loaded configuration seems incomplete. Missing 'resources' array.`));
43
+ }
44
+ if (!config.customization?.customComponentsDir) {
45
+ console.warn(chalk.yellow(`Warning: 'customization.customComponentsDir' is not defined in the config. Defaulting might occur elsewhere, but defining it is recommended.`));
46
+ }
47
+
48
+
49
+ console.log(chalk.dim(`Loaded configuration from ${configPath}`));
50
+ return config;
51
+
52
+ } catch (error) {
53
+ console.error(chalk.red(`\nError loading or parsing configuration file: ${configPath}`));
54
+ console.error(error);
55
+ process.exit(1);
56
+ }
57
+ }