@blu1606/create-walrus-app 0.1.0

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 (119) hide show
  1. package/dist/__tests__/helpers/adapter-compliance.d.ts +2 -0
  2. package/dist/__tests__/helpers/adapter-compliance.js +47 -0
  3. package/dist/__tests__/helpers/fixtures.d.ts +21 -0
  4. package/dist/__tests__/helpers/fixtures.js +30 -0
  5. package/dist/__tests__/helpers/fs-helpers.d.ts +12 -0
  6. package/dist/__tests__/helpers/fs-helpers.js +35 -0
  7. package/dist/__tests__/helpers/index.d.ts +4 -0
  8. package/dist/__tests__/helpers/index.js +4 -0
  9. package/dist/__tests__/helpers/test-hooks.d.ts +3 -0
  10. package/dist/__tests__/helpers/test-hooks.js +18 -0
  11. package/dist/context.d.ts +2 -0
  12. package/dist/context.js +43 -0
  13. package/dist/context.test.d.ts +1 -0
  14. package/dist/context.test.js +98 -0
  15. package/dist/generator/file-ops.d.ts +12 -0
  16. package/dist/generator/file-ops.js +40 -0
  17. package/dist/generator/index.d.ts +2 -0
  18. package/dist/generator/index.js +75 -0
  19. package/dist/generator/index.test.d.ts +1 -0
  20. package/dist/generator/index.test.js +143 -0
  21. package/dist/generator/layers.d.ts +3 -0
  22. package/dist/generator/layers.js +59 -0
  23. package/dist/generator/layers.test.d.ts +1 -0
  24. package/dist/generator/layers.test.js +92 -0
  25. package/dist/generator/merge.d.ts +14 -0
  26. package/dist/generator/merge.js +62 -0
  27. package/dist/generator/merge.test.d.ts +1 -0
  28. package/dist/generator/merge.test.js +79 -0
  29. package/dist/generator/transform.d.ts +21 -0
  30. package/dist/generator/transform.js +52 -0
  31. package/dist/generator/transform.test.d.ts +1 -0
  32. package/dist/generator/transform.test.js +51 -0
  33. package/dist/generator/types.d.ts +18 -0
  34. package/dist/generator/types.js +1 -0
  35. package/dist/index.d.ts +2 -0
  36. package/dist/index.js +106 -0
  37. package/dist/matrix.d.ts +31 -0
  38. package/dist/matrix.js +31 -0
  39. package/dist/matrix.test.d.ts +1 -0
  40. package/dist/matrix.test.js +70 -0
  41. package/dist/post-install/git.d.ts +12 -0
  42. package/dist/post-install/git.js +94 -0
  43. package/dist/post-install/index.d.ts +16 -0
  44. package/dist/post-install/index.js +56 -0
  45. package/dist/post-install/messages.d.ts +9 -0
  46. package/dist/post-install/messages.js +49 -0
  47. package/dist/post-install/package-manager.d.ts +14 -0
  48. package/dist/post-install/package-manager.js +57 -0
  49. package/dist/post-install/validator.d.ts +14 -0
  50. package/dist/post-install/validator.js +114 -0
  51. package/dist/prompts.d.ts +2 -0
  52. package/dist/prompts.js +115 -0
  53. package/dist/test-base.d.ts +1 -0
  54. package/dist/test-base.js +42 -0
  55. package/dist/types.d.ts +19 -0
  56. package/dist/types.js +1 -0
  57. package/dist/types.test.d.ts +1 -0
  58. package/dist/types.test.js +65 -0
  59. package/dist/utils/detect-pm.d.ts +2 -0
  60. package/dist/utils/detect-pm.js +10 -0
  61. package/dist/utils/detect-pm.test.d.ts +1 -0
  62. package/dist/utils/detect-pm.test.js +52 -0
  63. package/dist/utils/logger.d.ts +6 -0
  64. package/dist/utils/logger.js +7 -0
  65. package/dist/validator.d.ts +3 -0
  66. package/dist/validator.js +48 -0
  67. package/dist/validator.test.d.ts +1 -0
  68. package/dist/validator.test.js +96 -0
  69. package/package.json +68 -0
  70. package/templates/base/.env.example +31 -0
  71. package/templates/base/README.md +54 -0
  72. package/templates/base/package.json +19 -0
  73. package/templates/base/src/adapters/storage.ts +58 -0
  74. package/templates/base/src/types/index.ts +9 -0
  75. package/templates/base/src/types/walrus.ts +22 -0
  76. package/templates/base/src/utils/env.ts +41 -0
  77. package/templates/base/src/utils/format.ts +29 -0
  78. package/templates/base/tsconfig.json +19 -0
  79. package/templates/gallery/README.md +44 -0
  80. package/templates/gallery/package.json +6 -0
  81. package/templates/gallery/src/App.tsx +21 -0
  82. package/templates/gallery/src/components/FileCard.tsx +27 -0
  83. package/templates/gallery/src/components/GalleryGrid.tsx +30 -0
  84. package/templates/gallery/src/components/UploadModal.tsx +45 -0
  85. package/templates/gallery/src/styles.css +58 -0
  86. package/templates/gallery/src/types/gallery.ts +13 -0
  87. package/templates/gallery/src/utils/index-manager.ts +37 -0
  88. package/templates/react/.eslintrc.json +26 -0
  89. package/templates/react/README.md +80 -0
  90. package/templates/react/index.html +13 -0
  91. package/templates/react/package.json +32 -0
  92. package/templates/react/src/App.tsx +14 -0
  93. package/templates/react/src/components/Layout.tsx +21 -0
  94. package/templates/react/src/components/WalletConnect.tsx +21 -0
  95. package/templates/react/src/dapp-kit.css +1 -0
  96. package/templates/react/src/hooks/useStorage.ts +40 -0
  97. package/templates/react/src/hooks/useWallet.ts +16 -0
  98. package/templates/react/src/index.css +50 -0
  99. package/templates/react/src/index.ts +10 -0
  100. package/templates/react/src/main.tsx +17 -0
  101. package/templates/react/src/providers/QueryProvider.tsx +18 -0
  102. package/templates/react/src/providers/WalletProvider.tsx +37 -0
  103. package/templates/react/tsconfig.json +27 -0
  104. package/templates/react/tsconfig.node.json +10 -0
  105. package/templates/react/vite.config.ts +19 -0
  106. package/templates/sdk-mysten/README.md +65 -0
  107. package/templates/sdk-mysten/package.json +14 -0
  108. package/templates/sdk-mysten/src/adapter.ts +80 -0
  109. package/templates/sdk-mysten/src/client.ts +45 -0
  110. package/templates/sdk-mysten/src/config.ts +33 -0
  111. package/templates/sdk-mysten/src/index.ts +11 -0
  112. package/templates/sdk-mysten/src/types.ts +19 -0
  113. package/templates/sdk-mysten/test/adapter.test.ts +20 -0
  114. package/templates/simple-upload/README.md +24 -0
  115. package/templates/simple-upload/package.json +6 -0
  116. package/templates/simple-upload/src/App.tsx +27 -0
  117. package/templates/simple-upload/src/components/FilePreview.tsx +40 -0
  118. package/templates/simple-upload/src/components/UploadForm.tsx +51 -0
  119. package/templates/simple-upload/src/styles.css +33 -0
@@ -0,0 +1,2 @@
1
+ import type { StorageAdapter } from '../../../../base/src/adapters/storage.js';
2
+ export declare function testStorageAdapterCompliance(adapterName: string, adapter: StorageAdapter): void;
@@ -0,0 +1,47 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ export function testStorageAdapterCompliance(adapterName, adapter) {
3
+ describe(`${adapterName} StorageAdapter Compliance`, () => {
4
+ describe('Interface Implementation', () => {
5
+ it('should implement upload method', () => {
6
+ expect(adapter.upload).toBeDefined();
7
+ expect(typeof adapter.upload).toBe('function');
8
+ });
9
+ it('should implement download method', () => {
10
+ expect(adapter.download).toBeDefined();
11
+ expect(typeof adapter.download).toBe('function');
12
+ });
13
+ it('should implement getMetadata method', () => {
14
+ expect(adapter.getMetadata).toBeDefined();
15
+ expect(typeof adapter.getMetadata).toBe('function');
16
+ });
17
+ it('should implement delete method', () => {
18
+ expect(adapter.delete).toBeDefined();
19
+ expect(typeof adapter.delete).toBe('function');
20
+ });
21
+ });
22
+ describe('Type Signatures', () => {
23
+ it('upload should accept File and UploadOptions', async () => {
24
+ const mockFile = new File(['test'], 'test.txt', { type: 'text/plain' });
25
+ const mockOptions = { epochs: 1 };
26
+ await expect(async () => {
27
+ await adapter.upload(mockFile, mockOptions);
28
+ }).rejects.toThrow();
29
+ });
30
+ it('download should accept string blobId', async () => {
31
+ await expect(async () => {
32
+ await adapter.download('test-blob-id');
33
+ }).rejects.toThrow();
34
+ });
35
+ it('getMetadata should accept string blobId', async () => {
36
+ await expect(async () => {
37
+ await adapter.getMetadata('test-blob-id');
38
+ }).rejects.toThrow();
39
+ });
40
+ it('delete should accept string blobId', async () => {
41
+ await expect(async () => {
42
+ await adapter.delete('test-blob-id');
43
+ }).rejects.toThrow();
44
+ });
45
+ });
46
+ });
47
+ }
@@ -0,0 +1,21 @@
1
+ export declare const MOCK_PACKAGE_JSON: {
2
+ name: string;
3
+ version: string;
4
+ type: string;
5
+ scripts: {
6
+ dev: string;
7
+ build: string;
8
+ };
9
+ dependencies: {
10
+ react: string;
11
+ };
12
+ };
13
+ export declare const MOCK_TSCONFIG: {
14
+ compilerOptions: {
15
+ target: string;
16
+ module: string;
17
+ strict: boolean;
18
+ };
19
+ };
20
+ export declare const MOCK_VITE_CONFIG = "import { defineConfig } from 'vite';\nimport react from '@vitejs/plugin-react';\n\nexport default defineConfig({\n plugins: [react()],\n});\n";
21
+ export declare const MOCK_README = "# Test Project\n\nThis is a test project.\n";
@@ -0,0 +1,30 @@
1
+ export const MOCK_PACKAGE_JSON = {
2
+ name: 'test-app',
3
+ version: '0.1.0',
4
+ type: 'module',
5
+ scripts: {
6
+ dev: 'vite',
7
+ build: 'tsc && vite build',
8
+ },
9
+ dependencies: {
10
+ react: '^18.2.0',
11
+ },
12
+ };
13
+ export const MOCK_TSCONFIG = {
14
+ compilerOptions: {
15
+ target: 'ES2020',
16
+ module: 'ESNext',
17
+ strict: true,
18
+ },
19
+ };
20
+ export const MOCK_VITE_CONFIG = `import { defineConfig } from 'vite';
21
+ import react from '@vitejs/plugin-react';
22
+
23
+ export default defineConfig({
24
+ plugins: [react()],
25
+ });
26
+ `;
27
+ export const MOCK_README = `# Test Project
28
+
29
+ This is a test project.
30
+ `;
@@ -0,0 +1,12 @@
1
+ export declare function createTempDir(prefix?: string): Promise<string>;
2
+ export declare function cleanupTempDir(dir: string): Promise<void>;
3
+ export declare function createTestFile(dir: string, filename: string, content: string): Promise<string>;
4
+ export declare function readTestFile(filePath: string): Promise<string>;
5
+ export declare function createMockContext(overrides?: {}): {
6
+ projectName: string;
7
+ sdk: string;
8
+ framework: string;
9
+ useCase: string;
10
+ tailwind: boolean;
11
+ analytics: boolean;
12
+ };
@@ -0,0 +1,35 @@
1
+ import fs from 'fs-extra';
2
+ import path from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ const __filename = fileURLToPath(import.meta.url);
5
+ const __dirname = path.dirname(__filename);
6
+ export async function createTempDir(prefix = 'test-') {
7
+ const tempDir = path.join(__dirname, '../../../.tmp', `${prefix}${Date.now()}`);
8
+ await fs.ensureDir(tempDir);
9
+ return tempDir;
10
+ }
11
+ export async function cleanupTempDir(dir) {
12
+ if (await fs.pathExists(dir)) {
13
+ await fs.remove(dir);
14
+ }
15
+ }
16
+ export async function createTestFile(dir, filename, content) {
17
+ const filePath = path.join(dir, filename);
18
+ await fs.ensureDir(path.dirname(filePath));
19
+ await fs.writeFile(filePath, content, 'utf-8');
20
+ return filePath;
21
+ }
22
+ export async function readTestFile(filePath) {
23
+ return await fs.readFile(filePath, 'utf-8');
24
+ }
25
+ export function createMockContext(overrides = {}) {
26
+ return {
27
+ projectName: 'test-project',
28
+ sdk: 'mysten',
29
+ framework: 'react',
30
+ useCase: 'simple-upload',
31
+ tailwind: false,
32
+ analytics: false,
33
+ ...overrides,
34
+ };
35
+ }
@@ -0,0 +1,4 @@
1
+ export { createTempDir, cleanupTempDir, createTestFile, readTestFile, createMockContext, } from './fs-helpers.js';
2
+ export { useTempDirectory } from './test-hooks.js';
3
+ export { MOCK_PACKAGE_JSON, MOCK_TSCONFIG, MOCK_VITE_CONFIG, MOCK_README, } from './fixtures.js';
4
+ export { testStorageAdapterCompliance } from './adapter-compliance.js';
@@ -0,0 +1,4 @@
1
+ export { createTempDir, cleanupTempDir, createTestFile, readTestFile, createMockContext, } from './fs-helpers.js';
2
+ export { useTempDirectory } from './test-hooks.js';
3
+ export { MOCK_PACKAGE_JSON, MOCK_TSCONFIG, MOCK_VITE_CONFIG, MOCK_README, } from './fixtures.js';
4
+ export { testStorageAdapterCompliance } from './adapter-compliance.js';
@@ -0,0 +1,3 @@
1
+ export declare function useTempDirectory(): {
2
+ readonly dir: string;
3
+ };
@@ -0,0 +1,18 @@
1
+ import { afterEach, beforeEach } from 'vitest';
2
+ import { createTempDir, cleanupTempDir } from './fs-helpers.js';
3
+ export function useTempDirectory() {
4
+ let tempDir;
5
+ beforeEach(async () => {
6
+ tempDir = await createTempDir();
7
+ });
8
+ afterEach(async () => {
9
+ if (tempDir) {
10
+ await cleanupTempDir(tempDir);
11
+ }
12
+ });
13
+ return {
14
+ get dir() {
15
+ return tempDir;
16
+ },
17
+ };
18
+ }
@@ -0,0 +1,2 @@
1
+ import { Context } from './types.js';
2
+ export declare function buildContext(args: Record<string, unknown>, promptResults: Record<string, unknown>): Context;
@@ -0,0 +1,43 @@
1
+ import path from 'node:path';
2
+ import { detectPackageManager } from './utils/detect-pm.js';
3
+ export function buildContext(args, promptResults) {
4
+ const merged = { ...promptResults, ...args }; // Args override prompts
5
+ // Runtime validation before type assertions
6
+ const projectName = merged.projectName;
7
+ if (typeof projectName !== 'string' || !projectName) {
8
+ throw new Error('Project name is required and must be a string');
9
+ }
10
+ const sdk = merged.sdk;
11
+ if (sdk !== 'mysten' && sdk !== 'tusky' && sdk !== 'hibernuts') {
12
+ throw new Error(`Invalid SDK: ${sdk}. Must be one of: mysten, tusky, hibernuts`);
13
+ }
14
+ const framework = merged.framework;
15
+ if (framework !== 'react' &&
16
+ framework !== 'vue' &&
17
+ framework !== 'plain-ts') {
18
+ throw new Error(`Invalid framework: ${framework}. Must be one of: react, vue, plain-ts`);
19
+ }
20
+ const useCase = merged.useCase;
21
+ if (useCase !== 'simple-upload' &&
22
+ useCase !== 'gallery' &&
23
+ useCase !== 'defi-nft') {
24
+ throw new Error(`Invalid use case: ${useCase}. Must be one of: simple-upload, gallery, defi-nft`);
25
+ }
26
+ const packageManager = merged.packageManager || detectPackageManager();
27
+ if (packageManager !== 'npm' &&
28
+ packageManager !== 'pnpm' &&
29
+ packageManager !== 'yarn' &&
30
+ packageManager !== 'bun') {
31
+ throw new Error(`Invalid package manager: ${packageManager}. Must be one of: npm, pnpm, yarn, bun`);
32
+ }
33
+ return {
34
+ projectName,
35
+ projectPath: path.resolve(process.cwd(), projectName),
36
+ sdk,
37
+ framework,
38
+ useCase,
39
+ analytics: Boolean(merged.analytics),
40
+ tailwind: Boolean(merged.tailwind),
41
+ packageManager: packageManager,
42
+ };
43
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,98 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ import { buildContext } from './context.js';
3
+ import * as detectPmModule from './utils/detect-pm.js';
4
+ // Mock the detectPackageManager function
5
+ vi.mock('./utils/detect-pm.js', () => ({
6
+ detectPackageManager: vi.fn(() => 'pnpm'),
7
+ }));
8
+ describe('buildContext', () => {
9
+ beforeEach(() => {
10
+ vi.clearAllMocks();
11
+ });
12
+ it('should build context from args only', () => {
13
+ const args = {
14
+ projectName: 'test-app',
15
+ sdk: 'mysten',
16
+ framework: 'react',
17
+ useCase: 'simple-upload',
18
+ analytics: true,
19
+ tailwind: false,
20
+ };
21
+ const context = buildContext(args, {});
22
+ expect(context.projectName).toBe('test-app');
23
+ expect(context.sdk).toBe('mysten');
24
+ expect(context.framework).toBe('react');
25
+ expect(context.useCase).toBe('simple-upload');
26
+ expect(context.analytics).toBe(true);
27
+ expect(context.tailwind).toBe(false);
28
+ expect(context.packageManager).toBe('pnpm');
29
+ expect(context.projectPath).toMatch(/test-app$/);
30
+ });
31
+ it('should build context from prompt results only', () => {
32
+ const promptResults = {
33
+ projectName: 'prompt-app',
34
+ sdk: 'tusky',
35
+ framework: 'vue',
36
+ useCase: 'gallery',
37
+ analytics: false,
38
+ tailwind: true,
39
+ };
40
+ const context = buildContext({}, promptResults);
41
+ expect(context.projectName).toBe('prompt-app');
42
+ expect(context.sdk).toBe('tusky');
43
+ expect(context.framework).toBe('vue');
44
+ expect(context.useCase).toBe('gallery');
45
+ expect(context.analytics).toBe(false);
46
+ expect(context.tailwind).toBe(true);
47
+ });
48
+ it('should prioritize args over prompt results', () => {
49
+ const args = {
50
+ projectName: 'args-app',
51
+ sdk: 'mysten',
52
+ };
53
+ const promptResults = {
54
+ projectName: 'prompt-app',
55
+ sdk: 'tusky',
56
+ framework: 'react',
57
+ useCase: 'simple-upload',
58
+ analytics: true,
59
+ tailwind: true,
60
+ };
61
+ const context = buildContext(args, promptResults);
62
+ expect(context.projectName).toBe('args-app');
63
+ expect(context.sdk).toBe('mysten');
64
+ expect(context.framework).toBe('react');
65
+ expect(context.useCase).toBe('simple-upload');
66
+ });
67
+ it('should convert analytics to boolean correctly', () => {
68
+ const base = { sdk: 'mysten', framework: 'react', useCase: 'simple-upload' };
69
+ expect(buildContext({ projectName: 'test', analytics: true, ...base }, {}).analytics).toBe(true);
70
+ expect(buildContext({ projectName: 'test', analytics: false, ...base }, {}).analytics).toBe(false);
71
+ expect(buildContext({ projectName: 'test', analytics: 1, ...base }, {}).analytics).toBe(true);
72
+ expect(buildContext({ projectName: 'test', analytics: 0, ...base }, {}).analytics).toBe(false);
73
+ expect(buildContext({ projectName: 'test', analytics: 'yes', ...base }, {}).analytics).toBe(true);
74
+ expect(buildContext({ projectName: 'test', analytics: '', ...base }, {}).analytics).toBe(false);
75
+ });
76
+ it('should convert tailwind to boolean correctly', () => {
77
+ const base = { sdk: 'mysten', framework: 'react', useCase: 'simple-upload' };
78
+ expect(buildContext({ projectName: 'test', tailwind: true, ...base }, {}).tailwind).toBe(true);
79
+ expect(buildContext({ projectName: 'test', tailwind: false, ...base }, {}).tailwind).toBe(false);
80
+ expect(buildContext({ projectName: 'test', tailwind: 1, ...base }, {}).tailwind).toBe(true);
81
+ expect(buildContext({ projectName: 'test', tailwind: 0, ...base }, {}).tailwind).toBe(false);
82
+ });
83
+ it('should call detectPackageManager', () => {
84
+ buildContext({ projectName: 'test', sdk: 'mysten', framework: 'react', useCase: 'simple-upload' }, {});
85
+ expect(detectPmModule.detectPackageManager).toHaveBeenCalled();
86
+ });
87
+ it('should generate absolute projectPath', () => {
88
+ const context = buildContext({ projectName: 'test-app', sdk: 'mysten', framework: 'react', useCase: 'simple-upload' }, {});
89
+ expect(context.projectPath).toContain('test-app');
90
+ expect(context.projectPath.length).toBeGreaterThan('test-app'.length);
91
+ });
92
+ it('should handle partial args and prompts', () => {
93
+ const context = buildContext({ projectName: 'partial' }, { sdk: 'hibernuts', framework: 'react', useCase: 'simple-upload' });
94
+ expect(context.projectName).toBe('partial');
95
+ expect(context.sdk).toBe('hibernuts');
96
+ expect(context.framework).toBe('react');
97
+ });
98
+ });
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Recursively copy directory, excluding certain files
3
+ */
4
+ export declare function copyDirectory(src: string, dest: string, exclude?: string[]): Promise<number>;
5
+ /**
6
+ * Check if directory is empty
7
+ */
8
+ export declare function isDirectoryEmpty(dir: string): Promise<boolean>;
9
+ /**
10
+ * Create directory if it doesn't exist
11
+ */
12
+ export declare function ensureDirectory(dir: string): Promise<void>;
@@ -0,0 +1,40 @@
1
+ import fs from 'fs-extra';
2
+ import path from 'node:path';
3
+ /**
4
+ * Recursively copy directory, excluding certain files
5
+ */
6
+ export async function copyDirectory(src, dest, exclude = ['node_modules', '.git', 'dist']) {
7
+ let filesCreated = 0;
8
+ const entries = await fs.readdir(src, { withFileTypes: true });
9
+ for (const entry of entries) {
10
+ if (exclude.includes(entry.name))
11
+ continue;
12
+ const srcPath = path.join(src, entry.name);
13
+ const destPath = path.join(dest, entry.name);
14
+ if (entry.isDirectory()) {
15
+ await fs.ensureDir(destPath);
16
+ filesCreated += await copyDirectory(srcPath, destPath, exclude);
17
+ }
18
+ else {
19
+ await fs.copy(srcPath, destPath, { overwrite: true });
20
+ filesCreated++;
21
+ }
22
+ }
23
+ return filesCreated;
24
+ }
25
+ /**
26
+ * Check if directory is empty
27
+ */
28
+ export async function isDirectoryEmpty(dir) {
29
+ const exists = await fs.pathExists(dir);
30
+ if (!exists)
31
+ return true;
32
+ const entries = await fs.readdir(dir);
33
+ return entries.length === 0;
34
+ }
35
+ /**
36
+ * Create directory if it doesn't exist
37
+ */
38
+ export async function ensureDirectory(dir) {
39
+ await fs.ensureDir(dir);
40
+ }
@@ -0,0 +1,2 @@
1
+ import type { GeneratorOptions, GeneratorResult } from './types.js';
2
+ export declare function generateProject(options: GeneratorOptions): Promise<GeneratorResult>;
@@ -0,0 +1,75 @@
1
+ import path from 'node:path';
2
+ import fs from 'fs-extra';
3
+ import { logger } from '../utils/logger.js';
4
+ import { resolveLayers } from './layers.js';
5
+ import { copyDirectory, ensureDirectory, isDirectoryEmpty, } from './file-ops.js';
6
+ import { mergePackageJsonFiles } from './merge.js';
7
+ import { buildVariables, transformDirectory } from './transform.js';
8
+ export async function generateProject(options) {
9
+ const { context, targetDir, dryRun = false } = options;
10
+ try {
11
+ logger.info(`🏗️ Generating project: ${context.projectName}`);
12
+ // Pre-flight checks
13
+ if (!dryRun) {
14
+ const isEmpty = await isDirectoryEmpty(targetDir);
15
+ if (!isEmpty) {
16
+ throw new Error(`Directory ${targetDir} is not empty. Please use an empty directory.`);
17
+ }
18
+ await ensureDirectory(targetDir);
19
+ }
20
+ // Resolve layers
21
+ const layers = resolveLayers(context);
22
+ logger.info(`📦 Layers: ${layers.map((l) => l.name).join(' + ')}`);
23
+ let filesCreated = 0;
24
+ // Copy layers sequentially (later layers override)
25
+ for (const layer of layers) {
26
+ if (!(await fs.pathExists(layer.path))) {
27
+ logger.warn(`⚠️ Layer not found: ${layer.path} (skipping)`);
28
+ continue;
29
+ }
30
+ logger.info(`📁 Copying layer: ${layer.name}`);
31
+ if (!dryRun) {
32
+ const count = await copyDirectory(layer.path, targetDir);
33
+ filesCreated += count;
34
+ }
35
+ }
36
+ // Merge package.json from all layers
37
+ logger.info('🔗 Merging package.json files');
38
+ if (!dryRun) {
39
+ await mergePackageJsonFiles(layers.map((l) => l.path), path.join(targetDir, 'package.json'));
40
+ }
41
+ // Transform template variables
42
+ logger.info('✏️ Transforming template variables');
43
+ if (!dryRun) {
44
+ const vars = buildVariables(context);
45
+ await transformDirectory(targetDir, vars);
46
+ }
47
+ logger.success(`✓ Project generated successfully!`);
48
+ logger.info(`📂 Files created: ${filesCreated}`);
49
+ return {
50
+ success: true,
51
+ projectPath: targetDir,
52
+ filesCreated,
53
+ };
54
+ }
55
+ catch (error) {
56
+ logger.error(`Failed to generate project: ${error}`);
57
+ // Rollback: Remove partially created directory
58
+ if (!dryRun && (await fs.pathExists(targetDir))) {
59
+ logger.warn('🧹 Rolling back partial changes...');
60
+ try {
61
+ await fs.remove(targetDir);
62
+ }
63
+ catch (rollbackError) {
64
+ logger.error(`Failed to rollback: ${rollbackError}`);
65
+ logger.warn(`⚠️ Please manually delete the directory: ${targetDir}`);
66
+ }
67
+ }
68
+ return {
69
+ success: false,
70
+ projectPath: targetDir,
71
+ filesCreated: 0,
72
+ error: error,
73
+ };
74
+ }
75
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,143 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
+ import { generateProject } from './index.js';
3
+ import fs from 'fs-extra';
4
+ import path from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
6
+ const __filename = fileURLToPath(import.meta.url);
7
+ const __dirname = path.dirname(__filename);
8
+ describe('generateProject integration', () => {
9
+ const testOutputDir = path.join(__dirname, '../../test-output');
10
+ const templateDir = path.join(__dirname, '../../templates');
11
+ beforeEach(async () => {
12
+ // Clean up test output directory
13
+ await fs.remove(testOutputDir);
14
+ });
15
+ afterEach(async () => {
16
+ // Clean up after tests
17
+ await fs.remove(testOutputDir);
18
+ });
19
+ it('should generate project with all layers', async () => {
20
+ const context = {
21
+ projectName: 'test-walrus-app',
22
+ projectPath: path.join(testOutputDir, 'test-walrus-app'),
23
+ sdk: 'mysten',
24
+ framework: 'react',
25
+ useCase: 'simple-upload',
26
+ analytics: false,
27
+ tailwind: false,
28
+ packageManager: 'pnpm',
29
+ };
30
+ const result = await generateProject({
31
+ context,
32
+ templateDir,
33
+ targetDir: context.projectPath,
34
+ });
35
+ expect(result.success).toBe(true);
36
+ expect(result.filesCreated).toBeGreaterThan(0);
37
+ // Verify directory was created
38
+ const exists = await fs.pathExists(context.projectPath);
39
+ expect(exists).toBe(true);
40
+ // Verify package.json was merged
41
+ const pkgJsonPath = path.join(context.projectPath, 'package.json');
42
+ const pkgJsonExists = await fs.pathExists(pkgJsonPath);
43
+ expect(pkgJsonExists).toBe(true);
44
+ const pkgJson = await fs.readJson(pkgJsonPath);
45
+ expect(pkgJson.name).toBe('test-walrus-app');
46
+ expect(pkgJson.dependencies['@mysten/walrus']).toBeDefined();
47
+ expect(pkgJson.dependencies['react']).toBeDefined();
48
+ expect(pkgJson.scripts.dev).toBeDefined();
49
+ expect(pkgJson.scripts.upload).toBeDefined();
50
+ });
51
+ it('should transform template variables', async () => {
52
+ const context = {
53
+ projectName: 'my-custom-app',
54
+ projectPath: path.join(testOutputDir, 'my-custom-app'),
55
+ sdk: 'mysten',
56
+ framework: 'react',
57
+ useCase: 'simple-upload',
58
+ analytics: false,
59
+ tailwind: false,
60
+ packageManager: 'npm',
61
+ };
62
+ const result = await generateProject({
63
+ context,
64
+ templateDir,
65
+ targetDir: context.projectPath,
66
+ });
67
+ expect(result.success).toBe(true);
68
+ // Check README transformation
69
+ const readmePath = path.join(context.projectPath, 'README.md');
70
+ const readmeExists = await fs.pathExists(readmePath);
71
+ expect(readmeExists).toBe(true);
72
+ const readmeContent = await fs.readFile(readmePath, 'utf-8');
73
+ expect(readmeContent).toContain('my-custom-app');
74
+ expect(readmeContent).toContain('react');
75
+ expect(readmeContent).not.toContain('{{projectName}}');
76
+ expect(readmeContent).not.toContain('{{framework}}');
77
+ });
78
+ it('should fail for non-empty directory', async () => {
79
+ const context = {
80
+ projectName: 'test-app',
81
+ projectPath: path.join(testOutputDir, 'non-empty'),
82
+ sdk: 'mysten',
83
+ framework: 'react',
84
+ useCase: 'simple-upload',
85
+ analytics: false,
86
+ tailwind: false,
87
+ packageManager: 'pnpm',
88
+ };
89
+ // Create non-empty directory
90
+ await fs.ensureDir(context.projectPath);
91
+ await fs.writeFile(path.join(context.projectPath, 'existing.txt'), 'content');
92
+ const result = await generateProject({
93
+ context,
94
+ templateDir,
95
+ targetDir: context.projectPath,
96
+ });
97
+ expect(result.success).toBe(false);
98
+ expect(result.error).toBeDefined();
99
+ expect(result.error?.message).toContain('not empty');
100
+ });
101
+ it('should rollback on error', async () => {
102
+ const context = {
103
+ projectName: 'test-app',
104
+ projectPath: path.join(testOutputDir, 'rollback-test'),
105
+ sdk: 'mysten',
106
+ framework: 'react',
107
+ useCase: 'simple-upload',
108
+ analytics: false,
109
+ tailwind: false,
110
+ packageManager: 'pnpm',
111
+ };
112
+ // This will succeed because templates exist
113
+ const result = await generateProject({
114
+ context,
115
+ templateDir,
116
+ targetDir: context.projectPath,
117
+ });
118
+ // Just verify it works for now - rollback is tested via non-empty dir test
119
+ expect(result.success).toBe(true);
120
+ });
121
+ it('should handle dry run mode', async () => {
122
+ const context = {
123
+ projectName: 'dry-run-test',
124
+ projectPath: path.join(testOutputDir, 'dry-run'),
125
+ sdk: 'mysten',
126
+ framework: 'react',
127
+ useCase: 'simple-upload',
128
+ analytics: false,
129
+ tailwind: false,
130
+ packageManager: 'pnpm',
131
+ };
132
+ const result = await generateProject({
133
+ context,
134
+ templateDir,
135
+ targetDir: context.projectPath,
136
+ dryRun: true,
137
+ });
138
+ expect(result.success).toBe(true);
139
+ // Verify no files were actually created
140
+ const exists = await fs.pathExists(context.projectPath);
141
+ expect(exists).toBe(false);
142
+ });
143
+ });
@@ -0,0 +1,3 @@
1
+ import type { Context } from '../types.js';
2
+ import type { Layer } from './types.js';
3
+ export declare function resolveLayers(context: Context): Layer[];