@geekmidas/cli 1.10.34 → 1.10.35

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@geekmidas/cli",
3
- "version": "1.10.34",
3
+ "version": "1.10.35",
4
4
  "description": "CLI tools for building Lambda handlers, server applications, and generating OpenAPI specs",
5
5
  "private": false,
6
6
  "type": "module",
@@ -56,11 +56,11 @@
56
56
  "prompts": "~2.4.2",
57
57
  "tsx": "~4.20.3",
58
58
  "yaml": "~2.8.2",
59
+ "@geekmidas/constructs": "~3.0.9",
60
+ "@geekmidas/envkit": "~1.0.5",
59
61
  "@geekmidas/errors": "~1.0.0",
60
62
  "@geekmidas/logger": "~1.0.1",
61
- "@geekmidas/constructs": "~3.0.9",
62
- "@geekmidas/schema": "~1.0.0",
63
- "@geekmidas/envkit": "~1.0.5"
63
+ "@geekmidas/schema": "~1.0.0"
64
64
  },
65
65
  "devDependencies": {
66
66
  "@types/lodash.kebabcase": "^4.1.9",
@@ -892,6 +892,125 @@ describe('Workspace Dev Server', () => {
892
892
 
893
893
  expect(result.valid).toBe(true);
894
894
  });
895
+
896
+ it('should validate a properly configured Vite app', async () => {
897
+ const appDir = join(testDir, 'apps/web');
898
+ mkdirSync(appDir, { recursive: true });
899
+
900
+ writeFileSync(join(appDir, 'vite.config.ts'), 'export default {}');
901
+ writeFileSync(
902
+ join(appDir, 'package.json'),
903
+ JSON.stringify({
904
+ name: 'web',
905
+ devDependencies: { vite: '^5.0.0' },
906
+ scripts: { dev: 'vite' },
907
+ }),
908
+ );
909
+
910
+ const result = await validateFrontendApp('web', 'apps/web', testDir);
911
+
912
+ expect(result.valid).toBe(true);
913
+ expect(result.errors).toHaveLength(0);
914
+ });
915
+
916
+ it('should validate a properly configured Remix app', async () => {
917
+ const appDir = join(testDir, 'apps/web');
918
+ mkdirSync(appDir, { recursive: true });
919
+
920
+ writeFileSync(join(appDir, 'vite.config.ts'), 'export default {}');
921
+ writeFileSync(
922
+ join(appDir, 'package.json'),
923
+ JSON.stringify({
924
+ name: 'web',
925
+ devDependencies: {
926
+ '@remix-run/dev': '^2.0.0',
927
+ vite: '^5.0.0',
928
+ },
929
+ scripts: { dev: 'remix vite:dev' },
930
+ }),
931
+ );
932
+
933
+ const result = await validateFrontendApp('web', 'apps/web', testDir);
934
+
935
+ expect(result.valid).toBe(true);
936
+ expect(result.errors).toHaveLength(0);
937
+ });
938
+
939
+ it('should warn but stay valid for a custom frontend with a dev script', async () => {
940
+ const appDir = join(testDir, 'apps/web');
941
+ mkdirSync(appDir, { recursive: true });
942
+
943
+ // No recognized config, no recognized dep — but has a dev script
944
+ writeFileSync(
945
+ join(appDir, 'package.json'),
946
+ JSON.stringify({
947
+ name: 'web',
948
+ dependencies: { react: '^18.0.0' },
949
+ scripts: { dev: 'node serve.mjs' },
950
+ }),
951
+ );
952
+
953
+ const result = await validateFrontendApp('web', 'apps/web', testDir);
954
+
955
+ expect(result.valid).toBe(true);
956
+ expect(result.errors).toHaveLength(0);
957
+ expect(result.warnings).toContainEqual(
958
+ expect.stringContaining('No recognized frontend framework detected'),
959
+ );
960
+ });
961
+
962
+ it('should error for a custom frontend without a dev script', async () => {
963
+ const appDir = join(testDir, 'apps/web');
964
+ mkdirSync(appDir, { recursive: true });
965
+
966
+ writeFileSync(
967
+ join(appDir, 'package.json'),
968
+ JSON.stringify({
969
+ name: 'web',
970
+ dependencies: { react: '^18.0.0' },
971
+ }),
972
+ );
973
+
974
+ const result = await validateFrontendApp('web', 'apps/web', testDir);
975
+
976
+ expect(result.valid).toBe(false);
977
+ expect(result.errors).toContainEqual(
978
+ expect.stringContaining('No "dev" script found'),
979
+ );
980
+ });
981
+
982
+ it('should validate strictly against an explicit framework', async () => {
983
+ const appDir = join(testDir, 'apps/web');
984
+ mkdirSync(appDir, { recursive: true });
985
+
986
+ // Has next.config.js + next dep — auto-detect would say nextjs
987
+ writeFileSync(join(appDir, 'next.config.js'), 'module.exports = {}');
988
+ writeFileSync(
989
+ join(appDir, 'package.json'),
990
+ JSON.stringify({
991
+ name: 'web',
992
+ dependencies: { next: '^14.0.0' },
993
+ scripts: { dev: 'next dev' },
994
+ }),
995
+ );
996
+
997
+ // Caller explicitly declares Vite — should fail because vite
998
+ // config and vite dep are missing.
999
+ const result = await validateFrontendApp(
1000
+ 'web',
1001
+ 'apps/web',
1002
+ testDir,
1003
+ 'vite',
1004
+ );
1005
+
1006
+ expect(result.valid).toBe(false);
1007
+ expect(result.errors).toContainEqual(
1008
+ expect.stringContaining('Vite config file not found'),
1009
+ );
1010
+ expect(result.errors).toContainEqual(
1011
+ expect.stringContaining('Vite not found in dependencies'),
1012
+ );
1013
+ });
895
1014
  });
896
1015
 
897
1016
  describe('validateFrontendApps', () => {
package/src/dev/index.ts CHANGED
@@ -55,6 +55,7 @@ import type {
55
55
  TelescopeConfig,
56
56
  } from '../types';
57
57
  import {
58
+ type FrontendFramework,
58
59
  getAppBuildOrder,
59
60
  getDependencyEnvVars,
60
61
  type NormalizedWorkspace,
@@ -641,13 +642,79 @@ export function checkPortConflicts(
641
642
  }
642
643
 
643
644
  /**
644
- * Next.js config file patterns to check.
645
+ * Per-framework validation spec for supported frontend frameworks.
645
646
  */
646
- const NEXTJS_CONFIG_FILES = [
647
- 'next.config.js',
648
- 'next.config.ts',
649
- 'next.config.mjs',
650
- ];
647
+ interface FrontendFrameworkSpec {
648
+ displayName: string;
649
+ configFiles: string[];
650
+ dependency: string;
651
+ installCommand: string;
652
+ }
653
+
654
+ const FRONTEND_FRAMEWORKS: Record<FrontendFramework, FrontendFrameworkSpec> = {
655
+ nextjs: {
656
+ displayName: 'Next.js',
657
+ configFiles: [
658
+ 'next.config.js',
659
+ 'next.config.ts',
660
+ 'next.config.mjs',
661
+ 'next.config.cjs',
662
+ ],
663
+ dependency: 'next',
664
+ installCommand: 'pnpm add next react react-dom',
665
+ },
666
+ remix: {
667
+ displayName: 'Remix',
668
+ configFiles: [
669
+ 'remix.config.js',
670
+ 'remix.config.mjs',
671
+ 'remix.config.ts',
672
+ 'vite.config.js',
673
+ 'vite.config.ts',
674
+ 'vite.config.mjs',
675
+ 'vite.config.cjs',
676
+ ],
677
+ dependency: '@remix-run/dev',
678
+ installCommand: 'pnpm add -D @remix-run/dev',
679
+ },
680
+ vite: {
681
+ displayName: 'Vite',
682
+ configFiles: [
683
+ 'vite.config.js',
684
+ 'vite.config.ts',
685
+ 'vite.config.mjs',
686
+ 'vite.config.cjs',
687
+ ],
688
+ dependency: 'vite',
689
+ installCommand: 'pnpm add -D vite',
690
+ },
691
+ };
692
+
693
+ /**
694
+ * Auto-detect the frontend framework by scanning package.json deps and config
695
+ * files. Dependency match wins over config file match because deps are more
696
+ * distinctive (a Remix app has both vite.config.ts AND @remix-run/dev).
697
+ */
698
+ function detectFrontendFramework(
699
+ fullPath: string,
700
+ deps: Record<string, string>,
701
+ ): FrontendFramework | undefined {
702
+ const order: FrontendFramework[] = ['nextjs', 'remix', 'vite'];
703
+ for (const name of order) {
704
+ if (deps[FRONTEND_FRAMEWORKS[name].dependency]) {
705
+ return name;
706
+ }
707
+ }
708
+ for (const name of order) {
709
+ const hasConfig = FRONTEND_FRAMEWORKS[name].configFiles.some((file) =>
710
+ existsSync(join(fullPath, file)),
711
+ );
712
+ if (hasConfig) {
713
+ return name;
714
+ }
715
+ }
716
+ return undefined;
717
+ }
651
718
 
652
719
  /**
653
720
  * Validation result for a frontend app.
@@ -660,59 +727,88 @@ export interface FrontendValidationResult {
660
727
  }
661
728
 
662
729
  /**
663
- * Validate a frontend (Next.js) app configuration.
664
- * Checks for Next.js config file and dependency.
730
+ * Validate a frontend app configuration.
731
+ *
732
+ * If `framework` is provided, validates strictly against that framework's
733
+ * expected config file and dependency. Otherwise auto-detects the framework
734
+ * from package.json and config files. If no recognized framework is found, the
735
+ * app is allowed through with a warning provided it has a `dev` script.
736
+ *
665
737
  * @internal Exported for testing
666
738
  */
667
739
  export async function validateFrontendApp(
668
740
  appName: string,
669
741
  appPath: string,
670
742
  workspaceRoot: string,
743
+ framework?: FrontendFramework,
671
744
  ): Promise<FrontendValidationResult> {
672
745
  const errors: string[] = [];
673
746
  const warnings: string[] = [];
674
747
  const fullPath = join(workspaceRoot, appPath);
675
748
 
676
- // Check for Next.js config file
677
- const hasConfigFile = NEXTJS_CONFIG_FILES.some((file) =>
678
- existsSync(join(fullPath, file)),
679
- );
680
-
681
- if (!hasConfigFile) {
749
+ const packageJsonPath = join(fullPath, 'package.json');
750
+ if (!existsSync(packageJsonPath)) {
682
751
  errors.push(
683
- `Next.js config file not found. Expected one of: ${NEXTJS_CONFIG_FILES.join(', ')}`,
752
+ `package.json not found at ${appPath}. Run: pnpm init in the app directory.`,
684
753
  );
754
+ return { appName, valid: false, errors, warnings };
685
755
  }
686
756
 
687
- // Check for package.json
688
- const packageJsonPath = join(fullPath, 'package.json');
689
- if (existsSync(packageJsonPath)) {
690
- try {
691
- // eslint-disable-next-line @typescript-eslint/no-require-imports
692
- const pkg = require(packageJsonPath);
693
- const deps = { ...pkg.dependencies, ...pkg.devDependencies };
757
+ let pkg: {
758
+ dependencies?: Record<string, string>;
759
+ devDependencies?: Record<string, string>;
760
+ scripts?: Record<string, string>;
761
+ };
762
+ try {
763
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
764
+ pkg = require(packageJsonPath);
765
+ } catch {
766
+ errors.push(`Failed to read package.json at ${packageJsonPath}`);
767
+ return { appName, valid: false, errors, warnings };
768
+ }
694
769
 
695
- if (!deps.next) {
696
- errors.push(
697
- 'Next.js not found in dependencies. Run: pnpm add next react react-dom',
698
- );
699
- }
770
+ const deps: Record<string, string> = {
771
+ ...pkg.dependencies,
772
+ ...pkg.devDependencies,
773
+ };
700
774
 
701
- // Check for dev script
702
- if (!pkg.scripts?.dev) {
703
- warnings.push(
704
- 'No "dev" script found in package.json. Turbo expects a "dev" script to run.',
705
- );
706
- }
707
- } catch {
708
- errors.push(`Failed to read package.json at ${packageJsonPath}`);
775
+ const resolvedFramework =
776
+ framework ?? detectFrontendFramework(fullPath, deps);
777
+
778
+ if (resolvedFramework) {
779
+ const spec = FRONTEND_FRAMEWORKS[resolvedFramework];
780
+ const hasConfig = spec.configFiles.some((file) =>
781
+ existsSync(join(fullPath, file)),
782
+ );
783
+ if (!hasConfig) {
784
+ errors.push(
785
+ `${spec.displayName} config file not found. Expected one of: ${spec.configFiles.join(', ')}`,
786
+ );
787
+ }
788
+ if (!deps[spec.dependency]) {
789
+ errors.push(
790
+ `${spec.displayName} not found in dependencies. Run: ${spec.installCommand}`,
791
+ );
709
792
  }
710
793
  } else {
711
- errors.push(
712
- `package.json not found at ${appPath}. Run: pnpm init in the app directory.`,
794
+ warnings.push(
795
+ 'No recognized frontend framework detected (Next.js, Vite, Remix). ' +
796
+ 'The app will run via its "dev" script. Set `framework` in your app config to enable strict validation.',
713
797
  );
714
798
  }
715
799
 
800
+ if (!pkg.scripts?.dev) {
801
+ if (resolvedFramework) {
802
+ warnings.push(
803
+ 'No "dev" script found in package.json. Turbo expects a "dev" script to run.',
804
+ );
805
+ } else {
806
+ errors.push(
807
+ 'No "dev" script found in package.json. Without a recognized framework or a dev script, there is nothing to run.',
808
+ );
809
+ }
810
+ }
811
+
716
812
  return {
717
813
  appName,
718
814
  valid: errors.length === 0,
@@ -737,6 +833,7 @@ export async function validateFrontendApps(
737
833
  appName,
738
834
  app.path,
739
835
  workspace.root,
836
+ app.framework as FrontendFramework | undefined,
740
837
  );
741
838
  results.push(result);
742
839
  }
@@ -45,7 +45,7 @@ export const GEEKMIDAS_VERSIONS = {
45
45
  '@geekmidas/storage': '~2.0.2',
46
46
  '@geekmidas/studio': '~1.0.0',
47
47
  '@geekmidas/telescope': '~1.0.0',
48
- '@geekmidas/testkit': '~1.0.5',
48
+ '@geekmidas/testkit': '~1.0.6',
49
49
  '@geekmidas/cli': CLI_VERSION,
50
50
  } as const;
51
51