@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/CHANGELOG.md +6 -0
- package/dist/index.cjs +95 -22
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +95 -22
- package/dist/index.mjs.map +1 -1
- package/package.json +4 -4
- package/src/dev/__tests__/index.spec.ts +119 -0
- package/src/dev/index.ts +134 -37
- package/src/init/versions.ts +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@geekmidas/cli",
|
|
3
|
-
"version": "1.10.
|
|
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/
|
|
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
|
-
*
|
|
645
|
+
* Per-framework validation spec for supported frontend frameworks.
|
|
645
646
|
*/
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
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
|
|
664
|
-
*
|
|
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
|
-
|
|
677
|
-
|
|
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
|
-
`
|
|
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
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
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
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
}
|
|
770
|
+
const deps: Record<string, string> = {
|
|
771
|
+
...pkg.dependencies,
|
|
772
|
+
...pkg.devDependencies,
|
|
773
|
+
};
|
|
700
774
|
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
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
|
-
|
|
712
|
-
|
|
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
|
}
|
package/src/init/versions.ts
CHANGED
|
@@ -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.
|
|
48
|
+
'@geekmidas/testkit': '~1.0.6',
|
|
49
49
|
'@geekmidas/cli': CLI_VERSION,
|
|
50
50
|
} as const;
|
|
51
51
|
|