adminforth 2.25.1 → 2.26.0-next.10

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 (77) hide show
  1. package/commands/bundle.js +2 -1
  2. package/commands/createApp/templates/Dockerfile.hbs +12 -0
  3. package/commands/createApp/templates/package.json.hbs +18 -6
  4. package/commands/createApp/templates/pnpm_templates/pnpm-lock.yaml.hbs +8 -0
  5. package/commands/createApp/templates/pnpm_templates/pnpm-workspace.yaml.hbs +2 -0
  6. package/commands/createApp/templates/readme.md.hbs +23 -4
  7. package/commands/createApp/utils.js +107 -26
  8. package/commands/createPlugin/utils.js +5 -6
  9. package/commands/postinstall.js +1 -1
  10. package/dist/auth.d.ts.map +1 -1
  11. package/dist/auth.js.map +1 -1
  12. package/dist/dataConnectors/clickhouse.d.ts +4 -0
  13. package/dist/dataConnectors/clickhouse.d.ts.map +1 -1
  14. package/dist/dataConnectors/clickhouse.js +14 -0
  15. package/dist/dataConnectors/clickhouse.js.map +1 -1
  16. package/dist/dataConnectors/mongo.d.ts +4 -0
  17. package/dist/dataConnectors/mongo.d.ts.map +1 -1
  18. package/dist/dataConnectors/mongo.js +9 -0
  19. package/dist/dataConnectors/mongo.js.map +1 -1
  20. package/dist/dataConnectors/mysql.d.ts +4 -0
  21. package/dist/dataConnectors/mysql.d.ts.map +1 -1
  22. package/dist/dataConnectors/mysql.js +11 -0
  23. package/dist/dataConnectors/mysql.js.map +1 -1
  24. package/dist/dataConnectors/postgres.d.ts +4 -0
  25. package/dist/dataConnectors/postgres.d.ts.map +1 -1
  26. package/dist/dataConnectors/postgres.js +11 -0
  27. package/dist/dataConnectors/postgres.js.map +1 -1
  28. package/dist/dataConnectors/sqlite.d.ts +4 -0
  29. package/dist/dataConnectors/sqlite.d.ts.map +1 -1
  30. package/dist/dataConnectors/sqlite.js +11 -0
  31. package/dist/dataConnectors/sqlite.js.map +1 -1
  32. package/dist/index.d.ts +1 -0
  33. package/dist/index.d.ts.map +1 -1
  34. package/dist/index.js +3 -0
  35. package/dist/index.js.map +1 -1
  36. package/dist/modules/codeInjector.d.ts +11 -2
  37. package/dist/modules/codeInjector.d.ts.map +1 -1
  38. package/dist/modules/codeInjector.js +239 -82
  39. package/dist/modules/codeInjector.js.map +1 -1
  40. package/dist/modules/styles.d.ts +4 -0
  41. package/dist/modules/styles.d.ts.map +1 -1
  42. package/dist/modules/styles.js +4 -0
  43. package/dist/modules/styles.js.map +1 -1
  44. package/dist/modules/utils.d.ts.map +1 -1
  45. package/dist/servers/express.d.ts +0 -1
  46. package/dist/servers/express.d.ts.map +1 -1
  47. package/dist/spa/README.md +4 -4
  48. package/dist/spa/package-lock.json +1902 -1427
  49. package/dist/spa/package.json +4 -3
  50. package/dist/spa/pnpm-lock.yaml +3558 -0
  51. package/dist/spa/src/afcl/Button.vue +12 -5
  52. package/dist/spa/src/afcl/Dialog.vue +155 -126
  53. package/dist/spa/src/afcl/Modal.vue +31 -8
  54. package/dist/spa/src/afcl/Select.vue +6 -2
  55. package/dist/spa/src/components/AcceptModal.vue +31 -53
  56. package/dist/spa/src/components/MenuLink.vue +18 -4
  57. package/dist/spa/src/components/ResourceListTable.vue +11 -11
  58. package/dist/spa/src/components/ResourceListTableVirtual.vue +21 -26
  59. package/dist/spa/src/components/Sidebar.vue +1 -1
  60. package/dist/spa/src/components/ThreeDotsMenu.vue +1 -1
  61. package/dist/spa/src/components/ValueRenderer.vue +1 -1
  62. package/dist/spa/src/stores/core.ts +1 -2
  63. package/dist/spa/src/types/Back.ts +4 -3
  64. package/dist/spa/src/types/Common.ts +16 -0
  65. package/dist/spa/src/types/FrontendAPI.ts +0 -3
  66. package/dist/spa/src/utils/utils.ts +57 -2
  67. package/dist/spa/src/views/CreateView.vue +12 -11
  68. package/dist/spa/src/views/EditView.vue +9 -13
  69. package/dist/spa/vite.config.ts +29 -40
  70. package/dist/types/Back.d.ts +3 -4
  71. package/dist/types/Back.d.ts.map +1 -1
  72. package/dist/types/Back.js.map +1 -1
  73. package/dist/types/Common.d.ts +11 -0
  74. package/dist/types/Common.d.ts.map +1 -1
  75. package/dist/types/Common.js.map +1 -1
  76. package/package.json +14 -6
  77. package/scripts/postinstall.js +25 -0
@@ -5,9 +5,11 @@ import fsExtra from 'fs-extra';
5
5
  import os from 'os';
6
6
  import path from 'path';
7
7
  import { promisify } from 'util';
8
+ import yaml from 'yaml';
8
9
  import { ADMIN_FORTH_ABSOLUTE_PATH, getComponentNameFromPath, md5hash } from './utils.js';
9
10
  import { StylesGenerator } from './styleGenerator.js';
10
11
  import { afLogger } from '../modules/logger.js';
12
+ import { pathToFileURL } from 'url';
11
13
  let TMP_DIR;
12
14
  try {
13
15
  TMP_DIR = os.tmpdir();
@@ -60,9 +62,9 @@ function isFulfilled(result) {
60
62
  return result.status === 'fulfilled';
61
63
  }
62
64
  function notifyWatcherIssue(limit) {
63
- afLogger.info('Ran out of file handles after watching %s files.', limit);
64
- afLogger.info('Falling back to polling which uses more CPU.');
65
- afLogger.info('Run ulimit -n 10000 to increase the limit for open files.');
65
+ console.log('Ran out of file handles after watching %s files.', limit);
66
+ console.log('Falling back to polling which uses more CPU.');
67
+ console.log('Run ulimit -n 10000 to increase the limit for open files.');
66
68
  }
67
69
  class CodeInjector {
68
70
  spaTmpPath() {
@@ -76,7 +78,7 @@ class CodeInjector {
76
78
  const uniqueIcons = Array.from(new Set(icons));
77
79
  const collections = new Set(icons.map((icon) => icon.split(':')[0]));
78
80
  const iconPackageNames = Array.from(collections).map((collection) => `@iconify-prerendered/vue-${collection}`);
79
- const iconPackages = (await Promise.allSettled(iconPackageNames.map(async (pkg) => ({ pkg: await import(this.spaTmpPath() + '/node_modules/' + pkg), name: pkg }))));
81
+ const iconPackages = (await Promise.allSettled(iconPackageNames.map(async (pkg) => ({ pkg: await import(pathToFileURL(path.join(this.spaTmpPath(), 'node_modules', pkg)).href), name: pkg }))));
80
82
  const loadedIconPackages = iconPackages.filter(isFulfilled).map((res) => res.value).reduce((acc, { pkg, name }) => {
81
83
  acc[name.slice(`@iconify-prerendered/vue-`.length)] = pkg;
82
84
  return acc;
@@ -99,7 +101,7 @@ class CodeInjector {
99
101
  this.allComponentNames[filePath] = componentName;
100
102
  }
101
103
  cleanup() {
102
- afLogger.info('Cleaning up...');
104
+ console.log('Cleaning up...');
103
105
  this.allWatchers.forEach((watcher) => {
104
106
  watcher.removeAll();
105
107
  });
@@ -116,40 +118,68 @@ class CodeInjector {
116
118
  process.exit();
117
119
  }));
118
120
  }
119
- async runNpmShell({ command, cwd, envOverrides = {} }) {
121
+ async doesUserHasPnpmLockFile(dir) {
122
+ const usersPackagePath = path.join(dir, 'package.json');
123
+ let packageContent = null;
124
+ try {
125
+ packageContent = JSON.parse(await fs.promises.readFile(usersPackagePath, 'utf-8'));
126
+ }
127
+ catch (e) {
128
+ // user package.json does not exist, user does not have custom components
129
+ }
130
+ if (packageContent) {
131
+ const lockPath = path.join(dir, 'pnpm-lock.yaml');
132
+ let lock = null;
133
+ try {
134
+ lock = yaml.parse(await fs.promises.readFile(lockPath, 'utf-8'));
135
+ return true;
136
+ }
137
+ catch (e) {
138
+ return false;
139
+ }
140
+ }
141
+ return false;
142
+ }
143
+ async runPackageManagerShell({ command, cwd, envOverrides = {} }) {
120
144
  const nodeBinary = process.execPath; // Path to the Node.js binary running this script
121
- // On Windows, npm is npm.cmd, on Unix systems it's npm
122
- const npmExecutable = process.platform === 'win32' ? 'npm.cmd' : 'npm';
123
- const npmPath = path.join(path.dirname(nodeBinary), npmExecutable); // Path to the npm executable
145
+ const doesUserHavePnpmLock = await this.doesUserHasPnpmLockFile(this.adminforth.config.customization.customComponentsDir);
146
+ // On Windows, npm/pnpm is npm/pnpm.cmd, on Unix systems it's npm/pnpm
147
+ let packageExecutable;
148
+ if (doesUserHavePnpmLock) {
149
+ process.env.HEAVY_DEBUG && console.log(`User has pnpm-lock.yaml, using pnpm for installing custom components`);
150
+ packageExecutable = process.platform === 'win32' ? 'pnpm.cmd' : 'pnpm';
151
+ }
152
+ else {
153
+ process.env.HEAVY_DEBUG && console.log(`User does not have pnpm-lock.yaml, falling back to npm for installing custom components`);
154
+ packageExecutable = process.platform === 'win32' ? 'npm.cmd' : 'npm';
155
+ }
156
+ const packagePath = path.join(path.dirname(nodeBinary), packageExecutable); // Path to the package executable
124
157
  const env = Object.assign(Object.assign({ VITE_ADMINFORTH_PUBLIC_PATH: this.adminforth.config.baseUrl, FORCE_COLOR: '1' }, process.env), envOverrides);
125
- afLogger.trace(`⚙️ exec: npm ${command}`);
126
- afLogger.trace(`🪲 npm ${command} cwd: ${cwd}`);
127
- afLogger.trace(`npm ${command} done in`);
128
- // On Windows, execute npm.cmd directly; on Unix, use node + npm
158
+ process.env.HEAVY_DEBUG && console.log(`⚙️ exec: ${packageExecutable} ${command}`);
159
+ process.env.HEAVY_DEBUG && console.log(`🪲 ${packageExecutable} ${command} cwd: ${cwd}`);
129
160
  let execCommand;
130
161
  if (process.platform === 'win32') {
131
162
  // Quote path if it contains spaces
132
- const quotedNpmPath = npmPath.includes(' ') ? `"${npmPath}"` : npmPath;
133
- execCommand = `${quotedNpmPath} ${command}`;
163
+ const quotedPackagePath = packagePath.includes(' ') ? `"${packagePath}"` : packagePath;
164
+ execCommand = `${quotedPackagePath} ${command}`;
134
165
  }
135
166
  else {
136
167
  // Quote paths that contain spaces (for Unix systems)
137
168
  const quotedNodeBinary = nodeBinary.includes(' ') ? `"${nodeBinary}"` : nodeBinary;
138
- const quotedNpmPath = npmPath.includes(' ') ? `"${npmPath}"` : npmPath;
139
- execCommand = `${quotedNodeBinary} ${quotedNpmPath} ${command}`;
169
+ const quotedPackagePath = packagePath.includes(' ') ? `"${packagePath}"` : packagePath;
170
+ execCommand = `${quotedNodeBinary} ${quotedPackagePath} ${command}`;
140
171
  }
141
172
  const execOptions = {
142
173
  cwd,
143
174
  env,
144
175
  };
145
- // On Windows, use shell to execute .cmd files
146
176
  if (process.platform === 'win32') {
147
177
  execOptions.shell = true;
148
178
  }
149
- const { stdout: out, stderr: err } = await execAsync(execCommand, execOptions);
150
- afLogger.trace(`npm ${command} done in`);
179
+ const { stderr: err } = await execAsync(execCommand, execOptions);
180
+ process.env.HEAVY_DEBUG && console.log(`${packageExecutable} ${command} done in`);
151
181
  if (err) {
152
- afLogger.trace(`🪲npm ${command} errors/warnings: ${err}`);
182
+ process.env.HEAVY_DEBUG && console.log(`🪲${packageExecutable} ${command} errors/warnings: ${err}`);
153
183
  }
154
184
  }
155
185
  async rmTmpDir() {
@@ -164,7 +194,8 @@ class CodeInjector {
164
194
  getServeDir() {
165
195
  return path.join(this.getSpaDir(), 'dist');
166
196
  }
167
- async packagesFromNpm(dir) {
197
+ async packagesFromPnpm(dir) {
198
+ var _a;
168
199
  const usersPackagePath = path.join(dir, 'package.json');
169
200
  let packageContent = null;
170
201
  let lockHash = '';
@@ -176,26 +207,55 @@ class CodeInjector {
176
207
  // user package.json does not exist, user does not have custom components
177
208
  }
178
209
  if (packageContent) {
179
- const lockPath = path.join(dir, 'package-lock.json');
210
+ const lockPath = path.join(dir, 'pnpm-lock.yaml');
180
211
  let lock = null;
181
212
  try {
182
- lock = JSON.parse(await fs.promises.readFile(lockPath, 'utf-8'));
213
+ lock = yaml.parse(await fs.promises.readFile(lockPath, 'utf-8'));
183
214
  }
184
215
  catch (e) {
185
- throw new Error(`Custom package-lock.json does not exist in ${dir}, but package.json does.
186
- We can't determine version of packages without package-lock.json. Please run npm install in ${dir}`);
216
+ const npmLockPath = path.join(dir, 'package-lock.json');
217
+ let npmLock = null;
218
+ try {
219
+ npmLock = JSON.parse(await fs.promises.readFile(npmLockPath, 'utf-8'));
220
+ }
221
+ catch (npmLockError) {
222
+ throw new Error(`Custom pnpm-lock.yaml or package-lock.json does not exist in ${dir}, but package.json does.
223
+ We can't determine version of packages without pnpm-lock.yaml or package-lock.json. Please run pnpm install or npm install in ${dir}`);
224
+ }
225
+ lockHash = hashify(npmLock);
226
+ packages = [
227
+ ...Object.keys(packageContent.dependencies || {}),
228
+ ...Object.keys(packageContent.devDependencies || {})
229
+ ].reduce((acc, packageName) => {
230
+ var _a;
231
+ const pack = (_a = npmLock === null || npmLock === void 0 ? void 0 : npmLock.packages) === null || _a === void 0 ? void 0 : _a[`node_modules/${packageName}`];
232
+ if (!(pack === null || pack === void 0 ? void 0 : pack.version)) {
233
+ throw new Error(`Package ${packageName} is not in package-lock.json but is in package.json. Please run 'npm install' in ${dir}`);
234
+ }
235
+ acc.push(`${packageName}@${pack.version}`);
236
+ return acc;
237
+ }, []);
238
+ return [lockHash, packages];
187
239
  }
188
240
  lockHash = hashify(lock);
241
+ const importer = (_a = lock === null || lock === void 0 ? void 0 : lock.importers) === null || _a === void 0 ? void 0 : _a['.'];
242
+ if (!importer) {
243
+ throw new Error(`pnpm-lock.yaml in ${dir} does not contain importer ".". Please run pnpm install in ${dir}`);
244
+ }
245
+ const importerDeps = Object.assign(Object.assign(Object.assign({}, (importer.dependencies || {})), (importer.devDependencies || {})), (importer.optionalDependencies || {}));
189
246
  packages = [
190
- ...Object.keys(packageContent.dependencies || []),
191
- ...Object.keys(packageContent.devDependencies || [])
247
+ ...Object.keys(packageContent.dependencies || {}),
248
+ ...Object.keys(packageContent.devDependencies || {})
192
249
  ].reduce((acc, packageName) => {
193
- const pack = lock.packages[`node_modules/${packageName}`];
194
- if (!pack) {
195
- throw new Error(`Package ${packageName} is not in package-lock.json but is in package.json. Please run 'npm install' in ${dir}`);
250
+ const depInfo = importerDeps[packageName];
251
+ const raw = typeof depInfo === 'string'
252
+ ? depInfo
253
+ : ((depInfo === null || depInfo === void 0 ? void 0 : depInfo.version) || (depInfo === null || depInfo === void 0 ? void 0 : depInfo.specifier));
254
+ if (!raw) {
255
+ throw new Error(`Package ${packageName} is not in pnpm-lock.yaml but is in package.json. Please run 'pnpm install' in ${dir}`);
196
256
  }
197
- const version = pack.version;
198
- acc.push(`${packageName}@${version}`);
257
+ const cleaned = raw.includes('(') ? raw.split('(')[0] : raw;
258
+ acc.push(`${packageName}@${cleaned}`);
199
259
  return acc;
200
260
  }, []);
201
261
  }
@@ -219,7 +279,7 @@ class CodeInjector {
219
279
  dereference: true, // needed to dereference types
220
280
  // preserveTimestamps: true, // needed to not invalidate any caches
221
281
  });
222
- afLogger.trace(`🪲⚙️ fsExtra.copy copy single file, ${src}, ${dest}`);
282
+ process.env.HEAVY_DEBUG && console.log(`🪲⚙️ fsExtra.copy copy single file, ${src}, ${dest}`);
223
283
  }));
224
284
  }
225
285
  async migrateLegacyCustomLayout(oldMeta) {
@@ -322,7 +382,7 @@ class CodeInjector {
322
382
  collectAssetsFromMenu(this.adminforth.config.menu);
323
383
  registerSettingPages(this.adminforth.config.auth.userMenuSettingsPages);
324
384
  const spaDir = this.getSpaDir();
325
- afLogger.trace(`🪲⚙️ fsExtra.copy from ${spaDir} -> ${this.spaTmpPath()}`);
385
+ process.env.HEAVY_DEBUG && console.log(`🪲⚙️ fsExtra.copy from ${spaDir} -> ${this.spaTmpPath()}`);
326
386
  // try to rm <spa tmp path>/src/types directory
327
387
  try {
328
388
  await fs.promises.rm(path.join(this.spaTmpPath(), 'src', 'types'), { recursive: true });
@@ -337,7 +397,7 @@ class CodeInjector {
337
397
  const filterPasses = !src.includes(`${path.sep}adminforth${path.sep}spa${path.sep}node_modules`) && !src.includes(`${path.sep}adminforth${path.sep}spa${path.sep}dist`)
338
398
  && !src.includes(`${path.sep}dist${path.sep}spa${path.sep}node_modules`) && !src.includes(`${path.sep}dist${path.sep}spa${path.sep}dist`);
339
399
  if (!filterPasses) {
340
- afLogger.trace(`🪲⚙️ fsExtra.copy filtered out, ${src}`);
400
+ process.env.HEAVY_DEBUG && console.log(`🪲⚙️ fsExtra.copy filtered out, ${src}`);
341
401
  }
342
402
  return filterPasses;
343
403
  },
@@ -360,7 +420,7 @@ class CodeInjector {
360
420
  }
361
421
  for (const [src, dest] of Object.entries(this.srcFoldersToSync)) {
362
422
  const to = path.join(this.spaTmpPath(), 'src', 'custom', dest);
363
- afLogger.trace(`🪲⚙️ srcFoldersToSync: fsExtra.copy from ${src}, ${to}`);
423
+ process.env.HEAVY_DEBUG && console.log(`🪲⚙️ srcFoldersToSync: fsExtra.copy from ${src}, ${to}`);
364
424
  await fsExtra.copy(src, to, {
365
425
  recursive: true,
366
426
  dereference: true,
@@ -555,20 +615,28 @@ class CodeInjector {
555
615
  routerVueContent = routerVueContent.replace('/* IMPORTANT:ADMINFORTH ROUTES */', routes);
556
616
  await fs.promises.writeFile(routerVuePath, routerVueContent);
557
617
  /* hash checking */
558
- const spaPackageLockPath = path.join(this.spaTmpPath(), 'package-lock.json');
559
- const spaPackageLock = JSON.parse(await fs.promises.readFile(spaPackageLockPath, 'utf-8'));
560
- const spaLockHash = hashify(spaPackageLock);
618
+ let spaLockHash = '';
619
+ if (await this.doesUserHasPnpmLockFile(this.adminforth.config.customization.customComponentsDir)) {
620
+ const spaPnpmLockPath = path.join(this.spaTmpPath(), 'pnpm-lock.yaml');
621
+ const spaPnpmLock = yaml.parse(await fs.promises.readFile(spaPnpmLockPath, 'utf-8'));
622
+ spaLockHash = hashify(spaPnpmLock);
623
+ }
624
+ else {
625
+ const spaNpmLockPath = path.join(this.spaTmpPath(), 'package-lock.json');
626
+ const spaNpmLock = JSON.parse(await fs.promises.readFile(spaNpmLockPath, 'utf-8'));
627
+ spaLockHash = hashify(spaNpmLock);
628
+ }
561
629
  /* customPackageLock */
562
630
  let usersLockHash = '';
563
631
  let usersPackages = [];
564
632
  if ((_q = this.adminforth.config.customization) === null || _q === void 0 ? void 0 : _q.customComponentsDir) {
565
- [usersLockHash, usersPackages] = await this.packagesFromNpm(this.adminforth.config.customization.customComponentsDir);
633
+ [usersLockHash, usersPackages] = await this.packagesFromPnpm(this.adminforth.config.customization.customComponentsDir);
566
634
  }
567
635
  const pluginPackages = [];
568
636
  // for every installed plugin generate packages
569
637
  for (const plugin of this.adminforth.activatedPlugins) {
570
- afLogger.trace(`🔧 Checking packages for plugin, ${plugin.constructor.name}, ${plugin.customFolderPath}`);
571
- const [lockHash, packages] = await this.packagesFromNpm(plugin.customFolderPath);
638
+ process.env.HEAVY_DEBUG && console.log(`🔧 Checking packages for plugin, ${plugin.constructor.name}, ${plugin.customFolderPath}`);
639
+ const [lockHash, packages] = await this.packagesFromPnpm(plugin.customFolderPath);
572
640
  if (packages.length) {
573
641
  pluginPackages.push({
574
642
  pluginName: plugin.constructor.name,
@@ -586,19 +654,20 @@ class CodeInjector {
586
654
  const existingHash = await fs.promises.readFile(hashPath, 'utf-8');
587
655
  await this.checkIconNames(icons);
588
656
  if (existingHash === fullHash) {
589
- afLogger.trace(`🪲Hashes match, skipping npm ci/install, from file: ${existingHash}, actual: ${fullHash}`);
657
+ process.env.HEAVY_DEBUG && console.log(`🪲Hashes match, skipping pnpm install, from file: ${existingHash}, actual: ${fullHash}`);
590
658
  return;
591
659
  }
592
660
  else {
593
- afLogger.trace(`🪲 Hashes do not match: from file: ${existingHash} actual: ${fullHash}, proceeding with npm ci/install`);
661
+ process.env.HEAVY_DEBUG && console.log(`🪲 Hashes do not match: from file: ${existingHash} actual: ${fullHash}, proceeding with pnpm install`);
594
662
  }
595
663
  }
596
664
  catch (e) {
597
665
  // ignore
598
- afLogger.trace(`🪲Hash file does not exist, proceeding with npm ci/install, ${e}`);
666
+ process.env.HEAVY_DEBUG && console.log(`🪲Hash file does not exist, proceeding with pnpm install, ${e}`);
599
667
  }
600
- await this.runNpmShell({ command: 'ci', cwd: this.spaTmpPath(), envOverrides: {
601
- NODE_ENV: 'development' // othewrwise it will not install devDependencies which we still need, e.g for extract
668
+ // install --frozen-lockfile works for npm and pnpm
669
+ await this.runPackageManagerShell({ command: 'install --frozen-lockfile', cwd: this.spaTmpPath(), envOverrides: {
670
+ NODE_ENV: 'development' // otherwise it will not install devDependencies which we still need, e.g for extract
602
671
  } });
603
672
  const allPacks = [
604
673
  ...iconPackageNames,
@@ -614,11 +683,11 @@ class CodeInjector {
614
683
  });
615
684
  const allPacksUnique = Array.from(new Set(allPacksFiltered));
616
685
  if (allPacks.length) {
617
- const npmInstallCommand = `install ${allPacksUnique.join(' ')}`;
618
- await this.runNpmShell({
619
- command: npmInstallCommand, cwd: this.spaTmpPath(),
686
+ const packageManagerInstallCommand = `install ${allPacksUnique.join(' ')}`;
687
+ await this.runPackageManagerShell({
688
+ command: packageManagerInstallCommand, cwd: this.spaTmpPath(),
620
689
  envOverrides: {
621
- NODE_ENV: 'development' // othewrwise it will not install devDependencies which we still need, e.g for extract
690
+ NODE_ENV: 'development' // otherwise it will not install devDependencies which we still need, e.g for extract
622
691
  }
623
692
  });
624
693
  }
@@ -642,7 +711,7 @@ class CodeInjector {
642
711
  }
643
712
  };
644
713
  await collectDirectories(spaPath);
645
- afLogger.trace(`🪲🔎 Watch for: ${directories.join(',')}`);
714
+ process.env.HEAVY_DEBUG && console.log(`🪲🔎 Watch for: ${directories.join(',')}`);
646
715
  const watcher = filewatcher({ debounce: 30 });
647
716
  directories.forEach((dir) => {
648
717
  // read directory files and add to watcher, only files not directories
@@ -650,13 +719,13 @@ class CodeInjector {
650
719
  files.forEach((file) => {
651
720
  const fullPath = path.join(dir, file);
652
721
  if (fs.lstatSync(fullPath).isFile()) {
653
- afLogger.trace(`🪲🔎 Watch for file ${fullPath}`);
722
+ process.env.HEAVY_DEBUG && console.log(`🪲🔎 Watch for file ${fullPath}`);
654
723
  watcher.add(fullPath);
655
724
  }
656
725
  });
657
726
  });
658
727
  watcher.on('change', async (file) => {
659
- afLogger.trace(`🐛 File ${file} changed (SPA), preparing sources...`);
728
+ process.env.HEAVY_DEBUG && console.log(`🐛 File ${file} changed (SPA), preparing sources...`);
660
729
  await this.updatePartials({ filesUpdated: [file.replace(spaPath + path.sep, '')] });
661
730
  });
662
731
  watcher.on('fallback', notifyWatcherIssue);
@@ -671,7 +740,7 @@ class CodeInjector {
671
740
  await fs.promises.access(customComponentsDir, fs.constants.F_OK);
672
741
  }
673
742
  catch (e) {
674
- afLogger.trace(`🪲Custom components dir ${customComponentsDir} does not exist, skipping watching`);
743
+ process.env.HEAVY_DEBUG && console.log(`🪲Custom components dir ${customComponentsDir} does not exist, skipping watching`);
675
744
  return;
676
745
  }
677
746
  // get all subdirs
@@ -696,21 +765,21 @@ class CodeInjector {
696
765
  await collectDirectories(customComponentsDir);
697
766
  const watcher = filewatcher({ debounce: 30 });
698
767
  files.forEach((file) => {
699
- afLogger.trace(`🪲🔎 Watch for file ${file}`);
768
+ process.env.HEAVY_DEBUG && console.log(`🪲🔎 Watch for file ${file}`);
700
769
  watcher.add(file);
701
770
  });
702
- afLogger.trace(`🪲🔎 Watch for: ${directories.join(',')}`);
771
+ process.env.HEAVY_DEBUG && console.log(`🪲🔎 Watch for: ${directories.join(',')}`);
703
772
  watcher.on('change', async (fileOrDir) => {
704
773
  // copy one file
705
774
  const relativeFilename = fileOrDir.replace(customComponentsDir + path.sep, '');
706
- afLogger.trace(`🔎 fileOrDir ${fileOrDir} changed`);
707
- afLogger.trace(`🔎 relativeFilename ${relativeFilename}`);
708
- afLogger.trace(`🔎 customComponentsDir ${customComponentsDir}`);
709
- afLogger.trace(`🔎 destination ${destination}`);
775
+ process.env.HEAVY_DEBUG && console.log(`🔎 fileOrDir ${fileOrDir} changed`);
776
+ process.env.HEAVY_DEBUG && console.log(`🔎 relativeFilename ${relativeFilename}`);
777
+ process.env.HEAVY_DEBUG && console.log(`🔎 customComponentsDir ${customComponentsDir}`);
778
+ process.env.HEAVY_DEBUG && console.log(`🔎 destination ${destination}`);
710
779
  const isFile = fs.lstatSync(fileOrDir).isFile();
711
780
  if (isFile) {
712
781
  const destPath = path.join(this.spaTmpPath(), 'src', 'custom', destination, relativeFilename);
713
- afLogger.trace(`🔎 Copying file ${fileOrDir} to ${destPath}`);
782
+ process.env.HEAVY_DEBUG && console.log(`🔎 Copying file ${fileOrDir} to ${destPath}`);
714
783
  await fsExtra.copy(fileOrDir, destPath);
715
784
  return;
716
785
  }
@@ -728,7 +797,7 @@ class CodeInjector {
728
797
  }
729
798
  catch (e) {
730
799
  // file does not exist
731
- afLogger.trace(`🪲File ${filePath} does not exist, returning null`);
800
+ process.env.HEAVY_DEBUG && console.log(`🪲File ${filePath} does not exist, returning null`);
732
801
  return null;
733
802
  }
734
803
  }
@@ -738,7 +807,9 @@ class CodeInjector {
738
807
  const filePath = path.join(folderPath, file.name);
739
808
  // 🚫 Skip big files or files which might be dynamic
740
809
  if (file.name === 'node_modules' || file.name === 'dist' ||
741
- file.name === 'i18n-messages.json' || file.name === 'i18n-empty.json') {
810
+ file.name === 'i18n-messages.json' || file.name === 'i18n-empty.json' ||
811
+ file.name === 'hashes.json' || file.name === 'package.json' ||
812
+ file.name === 'pnpm-lock.yaml' || file.name === 'package-lock.json') {
742
813
  return '';
743
814
  }
744
815
  allFiles.push(filePath);
@@ -752,8 +823,49 @@ class CodeInjector {
752
823
  }));
753
824
  return md5hash(hashes.join(''));
754
825
  }
826
+ // Compute a map of file relative paths -> md5 hash of file contents and return it.
827
+ // Skips directories/files that are ignored by computeSourcesHash (node_modules, dist, i18n files).
828
+ async computeSourcesHashMap(folderPath = this.spaTmpPath(), rootFolder = this.spaTmpPath(), map = {}) {
829
+ const files = await fs.promises.readdir(folderPath, { withFileTypes: true });
830
+ await Promise.all(files.map(async (file) => {
831
+ const filePath = path.join(folderPath, file.name);
832
+ // 🚫 Skip big/dynamic folders or files
833
+ if (file.name === 'node_modules' || file.name === 'dist' ||
834
+ file.name === 'i18n-messages.json' || file.name === 'i18n-empty.json' ||
835
+ file.name === 'hashes.json' || file.name === 'package.json' ||
836
+ file.name === 'pnpm-lock.yaml' || file.name === 'package-lock.json') {
837
+ return;
838
+ }
839
+ if (file.isDirectory()) {
840
+ return await this.computeSourcesHashMap(filePath, rootFolder, map);
841
+ }
842
+ else {
843
+ try {
844
+ const content = await fs.promises.readFile(filePath, 'utf-8');
845
+ const hash = md5hash(content);
846
+ // store relative path using forward slashes for portability
847
+ const rel = path.relative(rootFolder, filePath).split(path.sep).join('/');
848
+ map[rel] = hash;
849
+ }
850
+ catch (e) {
851
+ // If a file can't be read (binary or permission), log and continue
852
+ process.env.HEAVY_DEBUG && console.log(`🪲File ${filePath} read error: ${e}`);
853
+ return;
854
+ }
855
+ }
856
+ }));
857
+ return map;
858
+ }
859
+ // Convenience helper: compute per-file hashes and save them into hashes.json in the spa tmp dir
860
+ async saveSourcesHashesToFile(outputFileName = 'hashes.json', hashMap = {}) {
861
+ const root = this.spaTmpPath();
862
+ const outPath = path.join(root, outputFileName);
863
+ await fs.promises.writeFile(outPath, JSON.stringify(hashMap, null, 2), 'utf-8');
864
+ process.env.HEAVY_DEBUG && console.log(`🪲 Saved sources hashes to ${outPath}`);
865
+ return outPath;
866
+ }
755
867
  async bundleNow({ hotReload = false }) {
756
- afLogger.info(`${this.adminforth.formatAdminForth()} Bundling ${hotReload ? 'and listening for changes (🔥 Hotreload)' : ' (no hot reload)'}`);
868
+ console.log(`${this.adminforth.formatAdminForth()} Bundling ${hotReload ? 'and listening for changes (🔥 Hotreload)' : ' (no hot reload)'}`);
757
869
  this.adminforth.runningHotReload = hotReload;
758
870
  await this.prepareSources();
759
871
  if (hotReload) {
@@ -771,14 +883,12 @@ class CodeInjector {
771
883
  const serveDir = this.getServeDir();
772
884
  const allFiles = [];
773
885
  const sourcesHash = await this.computeSourcesHash(this.spaTmpPath(), allFiles);
774
- afLogger.trace(`🪲🪲 allFiles:, ${JSON.stringify(allFiles.sort((a, b) => a.localeCompare(b)), null, 1)}`);
886
+ process.env.HEAVY_DEBUG && console.log(`🪲🪲 allFiles:, ${JSON.stringify(allFiles.sort((a, b) => a.localeCompare(b)), null, 1)}`);
775
887
  const buildHash = await this.tryReadFile(path.join(serveDir, '.adminforth_build_hash'));
776
888
  const messagesHash = await this.tryReadFile(path.join(serveDir, '.adminforth_messages_hash'));
777
889
  const skipBuild = buildHash === sourcesHash;
778
890
  const skipExtract = messagesHash === sourcesHash;
779
- afLogger.trace(`🪲 SPA build hash: ${buildHash}`);
780
- afLogger.trace(`🪲 SPA messages hash: ${messagesHash}`);
781
- afLogger.trace(`🪲 SPA sources hash: ${sourcesHash}`);
891
+ process.env.HEAVY_DEBUG && console.log(`🪲 SPA messages hash: ${messagesHash}`);
782
892
  if (!skipBuild) {
783
893
  // remove serveDir if exists
784
894
  try {
@@ -790,7 +900,7 @@ class CodeInjector {
790
900
  await fs.promises.mkdir(serveDir, { recursive: true });
791
901
  }
792
902
  if (!skipExtract) {
793
- await this.runNpmShell({ command: 'run i18n:extract', cwd });
903
+ await this.runPackageManagerShell({ command: 'run i18n:extract', cwd });
794
904
  // create serveDir if not exists
795
905
  await fs.promises.mkdir(serveDir, { recursive: true });
796
906
  // copy i18n messages to serve dir
@@ -799,51 +909,98 @@ class CodeInjector {
799
909
  await fs.promises.writeFile(path.join(serveDir, '.adminforth_messages_hash'), sourcesHash);
800
910
  }
801
911
  else {
802
- afLogger.info(`AdminForth i18n message extraction skipped — build already performed for the current sources.`);
912
+ console.log(`AdminForth i18n message extraction skipped — build already performed for the current sources.`);
803
913
  }
804
914
  if (!hotReload) {
805
915
  if (!skipBuild) {
916
+ console.log(`🪲 Build cache miss or outdated, building SPA...`);
917
+ let oldHashForFiles = null;
918
+ try {
919
+ oldHashForFiles = await fs.promises.readFile(path.join(this.spaTmpPath(), 'hashes.json'), 'utf-8');
920
+ }
921
+ catch (e) {
922
+ // ignore if file doesn't exist, it is only for debugging
923
+ console.log(`Build cache not found, building now (downtime) please consider running npx adminforth bundle at build time to avoid downtimes at runtime`);
924
+ }
925
+ const root = this.spaTmpPath();
926
+ const hashMap = await this.computeSourcesHashMap(root, root, {});
927
+ if (oldHashForFiles) {
928
+ const parsedOldHashForFiles = JSON.parse(oldHashForFiles);
929
+ const logsToDisplay = [];
930
+ logsToDisplay.push(`Build cache exists but is outdated:`);
931
+ for (const [file, hash] of Object.entries(hashMap)) {
932
+ if (!parsedOldHashForFiles[file]) {
933
+ logsToDisplay.push(` - file ${file} - does not exist in cache but exists in runtime`);
934
+ }
935
+ else if (parsedOldHashForFiles[file] !== hash) {
936
+ logsToDisplay.push(` - file ${file} - content in cache is different then in runtime`);
937
+ }
938
+ }
939
+ /**
940
+ * Currently we can't detect, if file was removed,
941
+ * because we can only add files to the tpm folder but not remove them,
942
+ * so if file existed before and now doesn't exist, we will not detect it
943
+ */
944
+ // for(const [file, hash] of Object.entries(parsedOldHashForFiles)) {
945
+ // console.log(`checking file ${file} in old hash: ${hash}`);
946
+ // console.log(`checking file ${file} in new hash: ${hashMap[file]}`);
947
+ // if (!hashMap[file]) {
948
+ // logsToDisplay.push(` - file ${file} - exists in cache but does not exist in runtime`);
949
+ // }
950
+ // }
951
+ logsToDisplay.push(`If you are running in production now, then the cache loss is a downtime issue.`);
952
+ logsToDisplay.push(`If you have npx adminforth bundle in build time, then this issue might be caused by conditional instantiation of plugins:`);
953
+ logsToDisplay.push(`Please avoid constructions like (process.env.SOME_KEY ? new Plugin(...) ) because if you will miss SOME_KEY in build time build cache and functionality fails.`);
954
+ if (logsToDisplay.length > 4) {
955
+ for (const log of logsToDisplay) {
956
+ console.log(log);
957
+ }
958
+ }
959
+ }
806
960
  // TODO probably add option to build with tsh check (plain 'build')
807
- await this.runNpmShell({ command: 'run build-only', cwd });
961
+ await this.runPackageManagerShell({ command: 'run build-only', cwd });
808
962
  // coy dist to serveDir
809
963
  await fsExtra.copy(path.join(cwd, 'dist'), serveDir, { recursive: true });
810
964
  // save hash
811
965
  await fs.promises.writeFile(path.join(serveDir, '.adminforth_build_hash'), sourcesHash);
966
+ // save sources hashes to file for later debugging if needed
967
+ await this.saveSourcesHashesToFile('hashes.json', hashMap);
812
968
  }
813
969
  else {
814
- afLogger.info(`Skipping AdminForth SPA bundling - already completed for the current sources.`);
970
+ console.log(`Skipping AdminForth SPA bundling - already completed for the current sources.`);
815
971
  }
816
972
  }
817
973
  else {
818
974
  const command = 'run dev';
819
- afLogger.info(`⚙️ spawn: npm ${command}...`);
975
+ const usersPackageManager = await this.doesUserHasPnpmLockFile(this.adminforth.config.customization.customComponentsDir) ? 'pnpm' : 'npm';
976
+ console.log(`⚙️ spawn: ${usersPackageManager} ${command}...`);
820
977
  if (process.env.VITE_ADMINFORTH_PUBLIC_PATH) {
821
- afLogger.info(`⚠️ Your VITE_ADMINFORTH_PUBLIC_PATH: ${process.env.VITE_ADMINFORTH_PUBLIC_PATH} has no effect`);
978
+ console.log(`⚠️ Your VITE_ADMINFORTH_PUBLIC_PATH: ${process.env.VITE_ADMINFORTH_PUBLIC_PATH} has no effect`);
822
979
  }
823
980
  const env = Object.assign({ VITE_ADMINFORTH_PUBLIC_PATH: this.adminforth.config.baseUrl, FORCE_COLOR: '1' }, process.env);
824
981
  const nodeBinary = process.execPath;
825
- const npmPath = path.join(path.dirname(nodeBinary), 'npm');
982
+ const packageManagerPath = path.join(path.dirname(nodeBinary), usersPackageManager);
826
983
  let devServer;
827
984
  if (process.platform === 'win32') {
828
- devServer = spawn('npm', command.split(' '), { cwd, env, shell: true });
985
+ devServer = spawn(usersPackageManager, command.split(' '), { cwd, env, shell: true });
829
986
  }
830
987
  else {
831
- devServer = spawn(`${nodeBinary}`, [`${npmPath}`, ...command.split(' ')], { cwd, env });
988
+ devServer = spawn(`${nodeBinary}`, [`${packageManagerPath}`, ...command.split(' ')], { cwd, env });
832
989
  }
833
990
  devServer.stdout.on('data', (data) => {
834
991
  if (data.includes('➜')) {
835
992
  // TODO: maybe better use our string "App port: 5174. HMR port: 5274", it is more reliable because vue might change their output
836
993
  // parse port from message " ➜ Local: http://localhost:xyz/"
837
994
  const s = stripAnsiCodes(data.toString());
838
- afLogger.trace(`🪲 devServer stdout ➜ (port detect): ${s}`);
995
+ process.env.HEAVY_DEBUG && console.log(`🪲 devServer stdout ➜ (port detect): ${s}`);
839
996
  const portMatch = s.match(/.+?http:\/\/.+?:(\d+).+?/m);
840
997
  if (portMatch) {
841
998
  this.devServerPort = parseInt(portMatch[1]);
842
999
  }
843
1000
  }
844
1001
  else {
845
- afLogger.trace(`[AdminForth SPA]:`);
846
- afLogger.trace(data.toString());
1002
+ process.env.HEAVY_DEBUG && console.log(`[AdminForth SPA]:`);
1003
+ process.env.HEAVY_DEBUG && console.log(data.toString());
847
1004
  }
848
1005
  });
849
1006
  devServer.stderr.on('data', (data) => {