forked from srcbookdev/srcbook
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge branch 'main' into feat/srcbookdev#210-Auto-install-deps-when-c…
…reating-a-new-TS-Srcbook-srcbookdev#210
- Loading branch information
Showing
144 changed files
with
4,225 additions
and
1,293 deletions.
There are no files selected for viewing
This file was deleted.
Oops, something went wrong.
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,4 @@ | ||
pnpm*.yaml | ||
packages/api/drizzle/* | ||
packages/api/apps/templates/**/* | ||
**/*.src.md |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -10,4 +10,5 @@ module.exports = { | |
globals: { | ||
Bun: false, | ||
}, | ||
ignorePatterns: ['apps/templates/**/*'], | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,71 @@ | ||
import { CodeLanguageType, randomid, type AppType } from '@srcbook/shared'; | ||
import { db } from '../db/index.mjs'; | ||
import { type App as DBAppType, apps as appsTable } from '../db/schema.mjs'; | ||
import { createViteApp, deleteViteApp, pathToApp } from './disk.mjs'; | ||
import { CreateAppSchemaType } from './schemas.mjs'; | ||
import { asc, desc, eq } from 'drizzle-orm'; | ||
import { npmInstall } from '../exec.mjs'; | ||
|
||
function toSecondsSinceEpoch(date: Date): number { | ||
return Math.floor(date.getTime() / 1000); | ||
} | ||
|
||
export function serializeApp(app: DBAppType): AppType { | ||
return { | ||
id: app.externalId, | ||
name: app.name, | ||
language: app.language as CodeLanguageType, | ||
createdAt: toSecondsSinceEpoch(app.createdAt), | ||
updatedAt: toSecondsSinceEpoch(app.updatedAt), | ||
}; | ||
} | ||
|
||
async function insert( | ||
attrs: Pick<DBAppType, 'name' | 'language' | 'externalId'>, | ||
): Promise<DBAppType> { | ||
const [app] = await db.insert(appsTable).values(attrs).returning(); | ||
return app!; | ||
} | ||
|
||
export async function createApp(data: CreateAppSchemaType): Promise<DBAppType> { | ||
const app = await insert({ | ||
name: data.name, | ||
language: data.language, | ||
externalId: randomid(), | ||
}); | ||
|
||
await createViteApp(app); | ||
|
||
// TODO: handle this better. | ||
// This should be done somewhere else and surface issues or retries. | ||
// Not awaiting here because it's "happening in the background". | ||
npmInstall({ | ||
cwd: pathToApp(app.externalId), | ||
stdout(data) { | ||
console.log(data.toString('utf8')); | ||
}, | ||
stderr(data) { | ||
console.error(data.toString('utf8')); | ||
}, | ||
onExit(code) { | ||
console.log(`npm install exit code: ${code}`); | ||
}, | ||
}); | ||
|
||
return app; | ||
} | ||
|
||
export async function deleteApp(id: string) { | ||
await db.delete(appsTable).where(eq(appsTable.externalId, id)); | ||
await deleteViteApp(id); | ||
} | ||
|
||
export function loadApps(sort: 'asc' | 'desc') { | ||
const sorter = sort === 'asc' ? asc : desc; | ||
return db.select().from(appsTable).orderBy(sorter(appsTable.updatedAt)); | ||
} | ||
|
||
export async function loadApp(id: string) { | ||
const [app] = await db.select().from(appsTable).where(eq(appsTable.externalId, id)); | ||
return app; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,170 @@ | ||
import fs from 'node:fs/promises'; | ||
import Path from 'node:path'; | ||
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'; | ||
|
||
export function pathToApp(id: string) { | ||
return Path.join(APPS_DIR, id); | ||
} | ||
|
||
function pathToTemplate(template: string) { | ||
return Path.resolve(fileURLToPath(import.meta.url), '..', 'templates', template); | ||
} | ||
|
||
export function deleteViteApp(id: string) { | ||
return fs.rm(pathToApp(id), { recursive: true }); | ||
} | ||
|
||
export async function createViteApp(app: DBAppType) { | ||
const appPath = pathToApp(app.externalId); | ||
|
||
// Use recursive because its parent directory may not exist. | ||
await fs.mkdir(appPath, { recursive: true }); | ||
|
||
// Scaffold all the necessary project files. | ||
await scaffold(app, appPath); | ||
|
||
return app; | ||
} | ||
|
||
async function scaffold(app: DBAppType, destDir: string) { | ||
const template = `react-${app.language}`; | ||
|
||
function write(file: string, content?: string) { | ||
const targetPath = Path.join(destDir, file); | ||
return content === undefined | ||
? copy(Path.join(templateDir, file), targetPath) | ||
: fs.writeFile(targetPath, content, 'utf-8'); | ||
} | ||
|
||
const templateDir = pathToTemplate(template); | ||
const files = await fs.readdir(templateDir); | ||
for (const file of files.filter((f) => f !== 'package.json')) { | ||
await write(file); | ||
} | ||
|
||
const [pkgContents, idxContents] = await Promise.all([ | ||
fs.readFile(Path.join(templateDir, 'package.json'), 'utf-8'), | ||
fs.readFile(Path.join(templateDir, 'index.html'), 'utf-8'), | ||
]); | ||
|
||
const pkg = JSON.parse(pkgContents); | ||
pkg.name = toValidPackageName(app.name); | ||
const updatedPkgContents = JSON.stringify(pkg, null, 2) + '\n'; | ||
|
||
const updatedIdxContents = idxContents.replace( | ||
/<title>.*<\/title>/, | ||
`<title>${app.name}</title>`, | ||
); | ||
|
||
await Promise.all([ | ||
write('package.json', updatedPkgContents), | ||
write('index.html', updatedIdxContents), | ||
]); | ||
} | ||
|
||
export function fileUpdated(app: DBAppType, file: FileType) { | ||
const path = Path.join(pathToApp(app.externalId), file.path); | ||
return fs.writeFile(path, file.source, 'utf-8'); | ||
} | ||
|
||
async function copy(src: string, dest: string) { | ||
const stat = await fs.stat(src); | ||
if (stat.isDirectory()) { | ||
return copyDir(src, dest); | ||
} else { | ||
return fs.copyFile(src, dest); | ||
} | ||
} | ||
|
||
async function copyDir(srcDir: string, destDir: string) { | ||
await fs.mkdir(destDir, { recursive: true }); | ||
const files = await fs.readdir(srcDir); | ||
for (const file of files) { | ||
const srcFile = Path.resolve(srcDir, file); | ||
const destFile = Path.resolve(destDir, file); | ||
await copy(srcFile, destFile); | ||
} | ||
} | ||
|
||
// TODO: This does not scale. | ||
export async function getProjectFiles(app: DBAppType) { | ||
const projectDir = Path.join(APPS_DIR, app.externalId); | ||
|
||
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 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 }; | ||
}), | ||
); | ||
} | ||
|
||
async function getDiskEntries(projectDir: string, options: { exclude: string[] }) { | ||
const result: { files: Dirent[]; directories: Dirent[] } = { | ||
files: [], | ||
directories: [], | ||
}; | ||
|
||
for (const entry of await fs.readdir(projectDir, { withFileTypes: true })) { | ||
if (options.exclude.includes(entry.name)) { | ||
continue; | ||
} | ||
|
||
if (entry.isFile()) { | ||
result.files.push(entry); | ||
} else { | ||
result.directories.push(entry); | ||
} | ||
} | ||
|
||
return result; | ||
} | ||
|
||
// TODO: This does not scale. | ||
// What's the best way to know whether a file is a "binary" | ||
// file or not? Inspecting bytes for invalid utf8? | ||
const TEXT_FILE_EXTENSIONS = [ | ||
'.ts', | ||
'.cts', | ||
'.mts', | ||
'.tsx', | ||
'.js', | ||
'.cjs', | ||
'.mjs', | ||
'.jsx', | ||
'.md', | ||
'.markdown', | ||
'.json', | ||
'.css', | ||
'.html', | ||
]; | ||
|
||
function isBinary(basename: string) { | ||
const isDotfile = basename.startsWith('.'); // Assume these are text for now, e.g., .gitignore | ||
const isTextFile = TEXT_FILE_EXTENSIONS.includes(Path.extname(basename)); | ||
return !(isDotfile || isTextFile); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
import z from 'zod'; | ||
|
||
export const CreateAppSchema = z.object({ | ||
name: z.string(), | ||
language: z.union([z.literal('typescript'), z.literal('javascript')]), | ||
prompt: z.string().optional(), | ||
}); | ||
|
||
export type CreateAppSchemaType = z.infer<typeof CreateAppSchema>; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
These templates were copied from https://github.com/vitejs/vite/tree/main/packages/create-vite |
38 changes: 38 additions & 0 deletions
38
packages/api/apps/templates/react-javascript/eslint.config.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
import js from '@eslint/js' | ||
import globals from 'globals' | ||
import react from 'eslint-plugin-react' | ||
import reactHooks from 'eslint-plugin-react-hooks' | ||
import reactRefresh from 'eslint-plugin-react-refresh' | ||
|
||
export default [ | ||
{ ignores: ['dist'] }, | ||
{ | ||
files: ['**/*.{js,jsx}'], | ||
languageOptions: { | ||
ecmaVersion: 2020, | ||
globals: globals.browser, | ||
parserOptions: { | ||
ecmaVersion: 'latest', | ||
ecmaFeatures: { jsx: true }, | ||
sourceType: 'module', | ||
}, | ||
}, | ||
settings: { react: { version: '18.3' } }, | ||
plugins: { | ||
react, | ||
'react-hooks': reactHooks, | ||
'react-refresh': reactRefresh, | ||
}, | ||
rules: { | ||
...js.configs.recommended.rules, | ||
...react.configs.recommended.rules, | ||
...react.configs['jsx-runtime'].rules, | ||
...reactHooks.configs.recommended.rules, | ||
'react/jsx-no-target-blank': 'off', | ||
'react-refresh/only-export-components': [ | ||
'warn', | ||
{ allowConstantExport: true }, | ||
], | ||
}, | ||
}, | ||
] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
<!doctype html> | ||
<html lang="en"> | ||
<head> | ||
<meta charset="UTF-8" /> | ||
<link rel="icon" type="image/svg+xml" href="/vite.svg" /> | ||
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> | ||
<title>Vite + React</title> | ||
</head> | ||
<body> | ||
<div id="root"></div> | ||
<script type="module" src="/src/main.jsx"></script> | ||
</body> | ||
</html> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
{ | ||
"name": "vite-react-starter", | ||
"private": true, | ||
"version": "0.0.0", | ||
"type": "module", | ||
"scripts": { | ||
"dev": "vite", | ||
"build": "vite build", | ||
"lint": "eslint .", | ||
"preview": "vite preview" | ||
}, | ||
"dependencies": { | ||
"react": "^18.3.1", | ||
"react-dom": "^18.3.1" | ||
}, | ||
"devDependencies": { | ||
"@eslint/js": "^9.10.0", | ||
"@types/react": "^18.3.6", | ||
"@types/react-dom": "^18.3.0", | ||
"@vitejs/plugin-react": "^4.3.1", | ||
"eslint": "^9.10.0", | ||
"eslint-plugin-react": "^7.36.1", | ||
"eslint-plugin-react-hooks": "^5.1.0-rc.0", | ||
"eslint-plugin-react-refresh": "^0.4.12", | ||
"globals": "^15.9.0", | ||
"vite": "^5.4.6" | ||
} | ||
} |
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Oops, something went wrong.