Skip to content

Commit

Permalink
Merge branch 'main' into feat/srcbookdev#210-Auto-install-deps-when-c…
Browse files Browse the repository at this point in the history
…reating-a-new-TS-Srcbook-srcbookdev#210
  • Loading branch information
Aswanth-c authored Oct 11, 2024
2 parents f2b78fb + ea9d6df commit 2d53577
Show file tree
Hide file tree
Showing 17 changed files with 1,357 additions and 167 deletions.
151 changes: 108 additions & 43 deletions packages/api/apps/disk.mts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,7 @@ import { fileURLToPath } from 'node:url';
import { type App as DBAppType } from '../db/schema.mjs';
import { APPS_DIR } from '../constants.mjs';
import { toValidPackageName } from './utils.mjs';
import { Dirent } from 'node:fs';
import { FileType } from '@srcbook/shared';
import { DirEntryType, FileEntryType, FileType } from '@srcbook/shared';

export function pathToApp(id: string) {
return Path.join(APPS_DIR, id);
Expand Down Expand Up @@ -91,57 +90,123 @@ async function copyDir(srcDir: string, destDir: string) {
}
}

// TODO: This does not scale.
export async function getProjectFiles(app: DBAppType) {
export async function loadDirectory(
app: DBAppType,
path: string,
excludes = ['node_modules', 'dist', '.git'],
): Promise<DirEntryType> {
const projectDir = Path.join(APPS_DIR, app.externalId);
const dirPath = Path.join(projectDir, path);
const entries = await fs.readdir(dirPath, { withFileTypes: true });

const { files, directories } = await getDiskEntries(projectDir, {
exclude: ['node_modules', 'dist'],
});

const nestedFiles = await Promise.all(
directories.flatMap(async (dir) => {
const entries = await fs.readdir(Path.join(projectDir, dir.name), {
withFileTypes: true,
recursive: true,
});
return entries.filter((entry) => entry.isFile());
}),
);

const entries = [...files, ...nestedFiles.flat()];

return Promise.all(
entries.map(async (entry) => {
const fullPath = Path.join(entry.parentPath, entry.name);
const children = entries
.filter((entry) => excludes.indexOf(entry.name) === -1)
.map((entry) => {
const fullPath = Path.join(dirPath, entry.name);
const relativePath = Path.relative(projectDir, fullPath);
const contents = await fs.readFile(fullPath);
const binary = isBinary(entry.name);
const source = !binary ? contents.toString('utf-8') : `TODO: handle this`;
return { path: relativePath, source, binary };
}),
);

if (entry.isDirectory()) {
return {
type: 'directory' as const,
name: entry.name,
path: relativePath,
children: null,
};
} else {
return {
type: 'file' as const,
name: entry.name,
path: relativePath,
};
}
});

const relativePath = Path.relative(projectDir, dirPath);
const basename = Path.basename(relativePath);

return {
type: 'directory' as const,
name: basename === '' ? '.' : basename,
path: relativePath === '' ? '.' : relativePath,
children: children,
};
}

async function getDiskEntries(projectDir: string, options: { exclude: string[] }) {
const result: { files: Dirent[]; directories: Dirent[] } = {
files: [],
directories: [],
export async function createDirectory(
app: DBAppType,
dirname: string,
basename: string,
): Promise<DirEntryType> {
const projectDir = Path.join(APPS_DIR, app.externalId);
const dirPath = Path.join(projectDir, dirname, basename);

await fs.mkdir(dirPath, { recursive: false });

const relativePath = Path.relative(projectDir, dirPath);

return {
type: 'directory' as const,
name: Path.basename(relativePath),
path: relativePath,
children: null,
};
}

for (const entry of await fs.readdir(projectDir, { withFileTypes: true })) {
if (options.exclude.includes(entry.name)) {
continue;
}
export async function loadFile(app: DBAppType, path: string): Promise<FileType> {
const projectDir = Path.join(APPS_DIR, app.externalId);
const filePath = Path.join(projectDir, path);
const relativePath = Path.relative(projectDir, filePath);
const basename = Path.basename(filePath);

if (entry.isFile()) {
result.files.push(entry);
} else {
result.directories.push(entry);
}
if (isBinary(basename)) {
return { path: relativePath, name: basename, source: `TODO: handle this`, binary: true };
} else {
return {
path: relativePath,
name: basename,
source: await fs.readFile(filePath, 'utf-8'),
binary: false,
};
}
}

return result;
export async function createFile(
app: DBAppType,
dirname: string,
basename: string,
source: string,
): Promise<FileEntryType> {
const projectDir = Path.join(APPS_DIR, app.externalId);
const filePath = Path.join(projectDir, dirname, basename);
await fs.writeFile(filePath, source, 'utf-8');
const relativePath = Path.relative(projectDir, filePath);
return { type: 'file' as const, path: relativePath, name: Path.basename(filePath) };
}

export function deleteFile(app: DBAppType, path: string) {
const filePath = Path.join(APPS_DIR, app.externalId, path);
return fs.rm(filePath);
}

export async function renameFile(
app: DBAppType,
path: string,
name: string,
): Promise<FileEntryType> {
const projectDir = Path.join(APPS_DIR, app.externalId);
const oldPath = Path.join(projectDir, path);
const dirname = Path.dirname(oldPath);
const newPath = Path.join(dirname, name);
await fs.rename(oldPath, newPath);

const relativePath = Path.relative(projectDir, newPath);
const basename = Path.basename(newPath);

return {
type: 'file' as const,
name: basename,
path: relativePath,
};
}

// TODO: This does not scale.
Expand Down
6 changes: 1 addition & 5 deletions packages/api/server/channels/app.mts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import WebSocketServer, {
type ConnectionContextType,
} from '../ws-client.mjs';
import { loadApp } from '../../apps/app.mjs';
import { fileUpdated, getProjectFiles, pathToApp } from '../../apps/disk.mjs';
import { fileUpdated, pathToApp } from '../../apps/disk.mjs';
import { vite } from '../../exec.mjs';

type AppContextType = MessageContextType<'appId'>;
Expand Down Expand Up @@ -126,9 +126,5 @@ export function register(wss: WebSocketServer) {
{ url: null, status: existingProcess ? 'running' : 'stopped' },
]),
);

for (const file of await getProjectFiles(app)) {
ws.send(JSON.stringify([topic, 'file', { file }]));
}
});
}
141 changes: 141 additions & 0 deletions packages/api/server/http.mts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,14 @@ import { EXAMPLE_SRCBOOKS } from '../srcbook/examples.mjs';
import { pathToSrcbook } from '../srcbook/path.mjs';
import { isSrcmdPath } from '../srcmd/paths.mjs';
import { loadApps, loadApp, createApp, serializeApp, deleteApp } from '../apps/app.mjs';
import {
deleteFile,
renameFile,
loadDirectory,
loadFile,
createFile,
createDirectory,
} from '../apps/disk.mjs';
import { CreateAppSchema } from '../apps/schemas.mjs';

const app: Application = express();
Expand Down Expand Up @@ -463,6 +471,139 @@ router.delete('/apps/:id', cors(), async (req, res) => {
}
});

router.options('/apps/:id/directories', cors());
router.get('/apps/:id/directories', cors(), async (req, res) => {
const { id } = req.params;

// TODO: validate and ensure path is not absolute
const path = typeof req.query.path === 'string' ? req.query.path : '.';

try {
const app = await loadApp(id);

if (!app) {
return res.status(404).json({ error: 'App not found' });
}

const directory = await loadDirectory(app, path);

return res.json({ data: directory });
} catch (e) {
return error500(res, e as Error);
}
});

router.options('/apps/:id/directories', cors());
router.post('/apps/:id/directories', cors(), async (req, res) => {
const { id } = req.params;

// TODO: validate and ensure path is not absolute
const { dirname, basename } = req.body;

try {
const app = await loadApp(id);

if (!app) {
return res.status(404).json({ error: 'App not found' });
}

const directory = await createDirectory(app, dirname, basename);

return res.json({ data: directory });
} catch (e) {
return error500(res, e as Error);
}
});

router.options('/apps/:id/files', cors());
router.get('/apps/:id/files', cors(), async (req, res) => {
const { id } = req.params;

// TODO: validate and ensure path is not absolute
const path = typeof req.query.path === 'string' ? req.query.path : '.';

try {
const app = await loadApp(id);

if (!app) {
return res.status(404).json({ error: 'App not found' });
}

const file = await loadFile(app, path);

return res.json({ data: file });
} catch (e) {
return error500(res, e as Error);
}
});

router.options('/apps/:id/files', cors());
router.post('/apps/:id/files', cors(), async (req, res) => {
const { id } = req.params;

// TODO: validate and ensure path is not absolute
const { dirname, basename, source } = req.body;

try {
const app = await loadApp(id);

if (!app) {
return res.status(404).json({ error: 'App not found' });
}

const file = await createFile(app, dirname, basename, source);

return res.json({ data: file });
} catch (e) {
return error500(res, e as Error);
}
});

router.options('/apps/:id/files', cors());
router.delete('/apps/:id/files', cors(), async (req, res) => {
const { id } = req.params;

// TODO: validate and ensure path is not absolute
const path = typeof req.query.path === 'string' ? req.query.path : '.';

try {
const app = await loadApp(id);

if (!app) {
return res.status(404).json({ error: 'App not found' });
}

await deleteFile(app, path);

return res.json({ data: { deleted: true } });
} catch (e) {
return error500(res, e as Error);
}
});

router.options('/apps/:id/files/rename', cors());
router.post('/apps/:id/files/rename', cors(), async (req, res) => {
const { id } = req.params;

// TODO: validate and ensure path is not absolute
const path = typeof req.query.path === 'string' ? req.query.path : '.';
const name = req.query.name as string;

try {
const app = await loadApp(id);

if (!app) {
return res.status(404).json({ error: 'App not found' });
}

const file = await renameFile(app, path, name);

return res.json({ data: file });
} catch (e) {
return error500(res, e as Error);
}
});

app.use('/api', router);

export default app;
1 change: 1 addition & 0 deletions packages/components/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
"@lezer/highlight": "^1.2.1",
"@lezer/javascript": "^1.4.17",
"@radix-ui/react-collapsible": "^1.1.0",
"@radix-ui/react-context-menu": "^2.2.2",
"@radix-ui/react-dialog": "^1.1.1",
"@radix-ui/react-dropdown-menu": "^2.1.1",
"@radix-ui/react-icons": "^1.3.0",
Expand Down
Loading

0 comments on commit 2d53577

Please sign in to comment.