diff --git a/packages/api/ai/generate.mts b/packages/api/ai/generate.mts index d3b49348..7e22c42c 100644 --- a/packages/api/ai/generate.mts +++ b/packages/api/ai/generate.mts @@ -34,7 +34,7 @@ const makeGenerateCellUserPrompt = (session: SessionType, insertIdx: number, que text: '==== INTRODUCE CELL HERE ====', }); - const inlineSrcbookWithPlaceholder = encode(cellsWithPlaceholder, session.metadata, { + const inlineSrcbookWithPlaceholder = encode(cellsWithPlaceholder, session.language, { inline: true, }); @@ -57,7 +57,7 @@ const makeGenerateCellEditUserPrompt = ( session: SessionType, cell: CodeCellType, ) => { - const inlineSrcbook = encode(session.cells, session.metadata, { inline: true }); + const inlineSrcbook = encode(session.cells, session.language, { inline: true }); const prompt = `==== BEGIN SRCBOOK ==== ${inlineSrcbook} @@ -129,7 +129,7 @@ export async function generateCells( ): Promise { const model = await getOpenAIModel(); - const systemPrompt = makeGenerateCellSystemPrompt(session.metadata.language); + const systemPrompt = makeGenerateCellSystemPrompt(session.language); const userPrompt = makeGenerateCellUserPrompt(session, insertIdx, query); const result = await generateText({ model: model, @@ -157,7 +157,7 @@ export async function generateCells( export async function generateCellEdit(query: string, session: SessionType, cell: CodeCellType) { const model = await getOpenAIModel(); - const systemPrompt = makeGenerateCellEditSystemPrompt(session.metadata.language); + const systemPrompt = makeGenerateCellEditSystemPrompt(session.language); const userPrompt = makeGenerateCellEditUserPrompt(query, session, cell); const result = await generateText({ model: model, diff --git a/packages/api/server/http.mts b/packages/api/server/http.mts index 795ae3da..9c81f27a 100644 --- a/packages/api/server/http.mts +++ b/packages/api/server/http.mts @@ -69,7 +69,7 @@ router.post('/srcbooks', cors(), async (req, res) => { } try { - const srcbookDir = await createSrcbook(name, { language }); + const srcbookDir = await createSrcbook(name, language); return res.json({ error: false, result: { name, path: srcbookDir } }); } catch (e) { const error = e as unknown as Error; diff --git a/packages/api/server/ws.mts b/packages/api/server/ws.mts index 197d4f89..6c36b1ea 100644 --- a/packages/api/server/ws.mts +++ b/packages/api/server/ws.mts @@ -250,7 +250,7 @@ async function depsInstall(payload: DepsInstallPayloadType) { wss.broadcast(`session:${updatedSession.id}`, 'cell:updated', { cell: updatedCell }); - if (updatedSession.metadata.language === 'typescript') { + if (updatedSession.language === 'typescript') { const mustCreateTsServer = !tsservers.has(updatedSession.id); // Make sure to handle the following case here: @@ -304,11 +304,7 @@ async function cellCreate(payload: CellCreatePayloadType) { // TODO: handle potential errors await addCell(session, cell, index); - if ( - session.metadata.language === 'typescript' && - cell.type === 'code' && - tsservers.has(session.id) - ) { + if (session.language === 'typescript' && cell.type === 'code' && tsservers.has(session.id)) { const tsserver = tsservers.get(session.id); tsserver.open({ @@ -385,11 +381,7 @@ async function cellUpdate(payload: CellUpdatePayloadType) { const cell = result.cell as CodeCellType; - if ( - session.metadata.language === 'typescript' && - cell.type === 'code' && - tsservers.has(session.id) - ) { + if (session.language === 'typescript' && cell.type === 'code' && tsservers.has(session.id)) { const tsserver = tsservers.get(session.id); // This isn't intended for renaming, so the filenames @@ -432,7 +424,7 @@ async function cellRename(payload: CellRenamePayloadType) { } if ( - session.metadata.language === 'typescript' && + session.language === 'typescript' && cellBeforeUpdate.type === 'code' && tsservers.has(session.id) ) { @@ -495,7 +487,7 @@ async function cellDelete(payload: CellDeletePayloadType) { if (cell.type === 'code') { removeCodeCellFromDisk(updatedSession.dir, cell.filename); - if (updatedSession.metadata.language === 'typescript' && tsservers.has(updatedSession.id)) { + if (updatedSession.language === 'typescript' && tsservers.has(updatedSession.id)) { const file = pathToCodeFile(updatedSession.dir, cell.filename); const tsserver = tsservers.get(updatedSession.id); tsserver.close({ file }); @@ -562,7 +554,7 @@ async function tsserverStart(payload: TsServerStartPayloadType) { throw new Error(`No session exists for session '${payload.sessionId}'`); } - if (session.metadata.language !== 'typescript') { + if (session.language !== 'typescript') { throw new Error(`tsserver can only be used with TypeScript Srcbooks.`); } diff --git a/packages/api/session.mts b/packages/api/session.mts index 7fdcf7ed..1107d052 100644 --- a/packages/api/session.mts +++ b/packages/api/session.mts @@ -56,12 +56,12 @@ export async function createSession(srcbookDir: string) { id: Path.basename(srcbookDir), dir: srcbookDir, cells: result.cells, - metadata: result.metadata, + language: result.language, openedAt: Date.now(), }; // TODO: Read from disk once we support editing tsconfig.json. - if (session.metadata.language === 'typescript') { + if (session.language === 'typescript') { session['tsconfig.json'] = buildTsconfigJson(); } @@ -93,9 +93,9 @@ export async function addCell( switch (cell.type) { case 'markdown': - return writeReadmeToDisk(session.dir, session.metadata, session.cells); + return writeReadmeToDisk(session.dir, session.language, session.cells); case 'code': - return writeCellToDisk(session.dir, session.metadata, session.cells, cell); + return writeCellToDisk(session.dir, session.language, session.cells, cell); } } @@ -108,7 +108,7 @@ export async function updateSession( const updatedSession = { ...session, ...updates }; sessions[id] = updatedSession; if (flush) { - await writeToDisk(updatedSession.dir, session.metadata, updatedSession.cells); + await writeToDisk(updatedSession.dir, session.language, updatedSession.cells); } return updatedSession; } @@ -118,7 +118,7 @@ export async function exportSrcmdFile(session: SessionType, destinationPath: str throw new Error(`Cannot export .src.md file: ${destinationPath} already exists`); } - return fs.writeFile(destinationPath, encode(session.cells, session.metadata, { inline: true })); + return fs.writeFile(destinationPath, encode(session.cells, session.language, { inline: true })); } export async function findSession(id: string): Promise { @@ -155,7 +155,7 @@ function updateTitleCell(session: SessionType, cell: TitleCellType, updates: any const attrs = TitleCellUpdateAttrsSchema.parse(updates); return updateCellWithRollback(session, cell, attrs, async (session) => { try { - await writeReadmeToDisk(session.dir, session.metadata, session.cells); + await writeReadmeToDisk(session.dir, session.language, session.cells); } catch (e) { console.error(e); return [{ message: 'An error occurred persisting files to disk' }]; @@ -167,7 +167,7 @@ function updateMarkdownCell(session: SessionType, cell: MarkdownCellType, update const attrs = MarkdownCellUpdateAttrsSchema.parse(updates); return updateCellWithRollback(session, cell, attrs, async (session) => { try { - await writeReadmeToDisk(session.dir, session.metadata, session.cells); + await writeReadmeToDisk(session.dir, session.language, session.cells); } catch (e) { console.error(e); return [{ message: 'An error occurred persisting files to disk' }]; @@ -181,7 +181,7 @@ function updatePackageJsonCell(session: SessionType, cell: PackageJsonCellType, try { await writeCellToDisk( session.dir, - session.metadata, + session.language, session.cells, updatedCell as PackageJsonCellType, ); @@ -202,7 +202,7 @@ async function updateCodeCell( try { await writeCellToDisk( session.dir, - session.metadata, + session.language, session.cells, updatedCell as CodeCellType, ); @@ -235,14 +235,12 @@ export async function updateCodeCellFilename( }; } - const language = session.metadata.language; - - if (language !== languageFromFilename(filename)) { + if (session.language !== languageFromFilename(filename)) { return { success: false, errors: [ { - message: `File must have one of the following extensions: ${extensionsForLanguage(language)}`, + message: `File must have one of the following extensions: ${extensionsForLanguage(session.language)}`, attribute: 'filename', }, ], @@ -260,7 +258,7 @@ export async function updateCodeCellFilename( try { await moveCodeCellOnDisk( session.dir, - session.metadata, + session.language, session.cells, updatedCell as CodeCellType, cell.filename, @@ -286,22 +284,18 @@ export function updateCell(session: SessionType, cell: CellType, updates: CellUp } export function sessionToResponse(session: SessionType) { - if (session.metadata.language === 'typescript') { - return { - id: session.id, - cells: session.cells, - metadata: session.metadata, - 'tsconfig.json': session['tsconfig.json'], - openedAt: session.openedAt, - }; - } else { - return { - id: session.id, - cells: session.cells, - metadata: session.metadata, - openedAt: session.openedAt, - }; + const result: Pick = { + id: session.id, + cells: session.cells, + language: session.language, + openedAt: session.openedAt, + }; + + if (session.language === 'typescript') { + result['tsconfig.json'] = session['tsconfig.json']; } + + return result; } export async function readPackageJsonContentsFromDisk(session: SessionType) { diff --git a/packages/api/srcbook/index.mts b/packages/api/srcbook/index.mts index 8fc4ddeb..dc6dcb3b 100644 --- a/packages/api/srcbook/index.mts +++ b/packages/api/srcbook/index.mts @@ -5,7 +5,6 @@ import type { CodeCellType, CodeLanguageType, PackageJsonCellType, - SrcbookMetadataType, } from '@srcbook/shared'; import { randomid } from '@srcbook/shared'; import { encode, decode } from '../srcmd.mjs'; @@ -25,8 +24,8 @@ function writeCellOnlyToDisk(srcbookDir: string, cell: PackageJsonCellType | Cod return fs.writeFile(path, cell.source, { encoding: 'utf8' }); } -export function writeToDisk(srcbookDir: string, metadata: SrcbookMetadataType, cells: CellType[]) { - const writes = [writeReadmeToDisk(srcbookDir, metadata, cells)]; +export function writeToDisk(srcbookDir: string, language: CodeLanguageType, cells: CellType[]) { + const writes = [writeReadmeToDisk(srcbookDir, language, cells)]; for (const cell of cells) { if (cell.type === 'package.json' || cell.type === 'code') { @@ -39,26 +38,26 @@ export function writeToDisk(srcbookDir: string, metadata: SrcbookMetadataType, c export function writeCellToDisk( srcbookDir: string, - metadata: SrcbookMetadataType, + language: CodeLanguageType, cells: CellType[], cell: PackageJsonCellType | CodeCellType, ) { // Readme must also be updated return Promise.all([ - writeReadmeToDisk(srcbookDir, metadata, cells), + writeReadmeToDisk(srcbookDir, language, cells), writeCellOnlyToDisk(srcbookDir, cell), ]); } export function moveCodeCellOnDisk( srcbookDir: string, - metadata: SrcbookMetadataType, + language: CodeLanguageType, cells: CellType[], cell: CodeCellType, oldFilename: string, ) { return Promise.all([ - writeReadmeToDisk(srcbookDir, metadata, cells), + writeReadmeToDisk(srcbookDir, language, cells), fs.unlink(pathToCodeFile(srcbookDir, oldFilename)), fs.writeFile(pathToCodeFile(srcbookDir, cell.filename), cell.source, { encoding: 'utf8' }), ]); @@ -66,10 +65,10 @@ export function moveCodeCellOnDisk( export function writeReadmeToDisk( srcbookDir: string, - metadata: SrcbookMetadataType, + language: CodeLanguageType, cells: CellType[], ) { - return fs.writeFile(pathToReadme(srcbookDir), encode(cells, metadata, { inline: false }), { + return fs.writeFile(pathToReadme(srcbookDir), encode(cells, language, { inline: false }), { encoding: 'utf8', }); } @@ -107,9 +106,9 @@ export async function importSrcbookFromSrcmdText(text: string, directoryBasename throw new Error(`Cannot decode invalid srcmd`); } - const dirname = await createSrcbookDir(result.metadata.language, directoryBasename); + const dirname = await createSrcbookDir(result.language, directoryBasename); - await writeToDisk(dirname, result.metadata, result.cells); + await writeToDisk(dirname, result.language, result.cells); return dirname; } @@ -120,8 +119,8 @@ export async function importSrcbookFromSrcmdText(text: string, directoryBasename * This private directory has a randomid() private identifier. * Users are not supposed to be aware or modify private directories. */ -export async function createSrcbook(title: string, metadata: SrcbookMetadataType) { - const dirname = await createSrcbookDir(metadata.language); +export async function createSrcbook(title: string, language: CodeLanguageType) { + const dirname = await createSrcbookDir(language); const cells: CellType[] = [ { @@ -132,13 +131,13 @@ export async function createSrcbook(title: string, metadata: SrcbookMetadataType { id: randomid(), type: 'package.json', - source: buildPackageJson(metadata.language), + source: buildPackageJson(language), filename: 'package.json', status: 'idle', }, ]; - await writeToDisk(dirname, metadata, cells); + await writeToDisk(dirname, language, cells); return dirname; } diff --git a/packages/api/srcmd.mts b/packages/api/srcmd.mts index 2de540cb..aee7e96a 100644 --- a/packages/api/srcmd.mts +++ b/packages/api/srcmd.mts @@ -52,7 +52,7 @@ export async function decodeDir(dir: string): Promise { // Wait for all file reads to complete await Promise.all(pendingFileReads); - return { error: false, metadata: readmeResult.metadata, cells }; + return { error: false, language: readmeResult.language, cells }; } catch (e) { const error = e as unknown as Error; return { error: true, errors: [error.message] }; diff --git a/packages/api/srcmd/decoding.mts b/packages/api/srcmd/decoding.mts index ac0b2f4d..c165bc48 100644 --- a/packages/api/srcmd/decoding.mts +++ b/packages/api/srcmd/decoding.mts @@ -42,7 +42,7 @@ export function decode(contents: string): DecodeResult { // Finally, return either the set of errors or the tokens converted to cells if no errors were found. return errors.length > 0 ? { error: true, errors: errors } - : { error: false, metadata, cells: convertToCells(groups) }; + : { error: false, language: metadata.language, cells: convertToCells(groups) }; } /** @@ -50,7 +50,7 @@ export function decode(contents: string): DecodeResult { * * For example, we generate a subset of a Srcbook (1 or more cells) using AI. * When that happens, we do not have the entire .src.md contents, so we need - * to ignore some aspects of it, like parsing the metadata. + * to ignore some aspects of it, like parsing the srcbook metadata comment. */ export function decodeCells(contents: string): DecodeCellsResult { const tokens = marked.lexer(contents); diff --git a/packages/api/srcmd/encoding.mts b/packages/api/srcmd/encoding.mts index 1f570989..0510e166 100644 --- a/packages/api/srcmd/encoding.mts +++ b/packages/api/srcmd/encoding.mts @@ -3,14 +3,14 @@ import type { MarkdownCellType, PackageJsonCellType, TitleCellType, - SrcbookMetadataType, CellWithPlaceholderType, PlaceholderCellType, + CodeLanguageType, } from '@srcbook/shared'; export function encode( allCells: CellWithPlaceholderType[], - metadata: SrcbookMetadataType, + language: CodeLanguageType, options: { inline: boolean }, ) { const [firstCell, secondCell, ...remainingCells] = allCells; @@ -19,7 +19,7 @@ export function encode( const cells = remainingCells as (MarkdownCellType | CodeCellType | PlaceholderCellType)[]; const encoded = [ - ``, + ``, encodeTitleCell(titleCell), encodePackageJsonCell(packageJsonCell, options), ...cells.map((cell) => { diff --git a/packages/api/srcmd/types.mts b/packages/api/srcmd/types.mts index d4ed1c2f..7afc3117 100644 --- a/packages/api/srcmd/types.mts +++ b/packages/api/srcmd/types.mts @@ -1,4 +1,4 @@ -import { type CellType, type SrcbookMetadataType } from '@srcbook/shared'; +import { type CellType, type CodeLanguageType } from '@srcbook/shared'; export type DecodeErrorResult = { error: true; @@ -8,11 +8,11 @@ export type DecodeErrorResult = { export type DecodeSuccessResult = { error: false; cells: CellType[]; - metadata: SrcbookMetadataType; + language: CodeLanguageType; }; // This represents the result of decoding a complete .src.md file. export type DecodeResult = DecodeErrorResult | DecodeSuccessResult; // This represents the result of decoding a subset of content from a .src.md file. -export type DecodeCellsResult = DecodeErrorResult | Omit; +export type DecodeCellsResult = DecodeErrorResult | Omit; diff --git a/packages/api/test/srcmd.test.mts b/packages/api/test/srcmd.test.mts index e9968f34..7f36dfc8 100644 --- a/packages/api/test/srcmd.test.mts +++ b/packages/api/test/srcmd.test.mts @@ -87,7 +87,7 @@ describe('encoding and decoding srcmd files', () => { it('can encode cells', () => { const result = decode(srcmd) as DecodeSuccessResult; expect(result.error).toBe(false); - expect(encode(result.cells, result.metadata, { inline: true })).toEqual(srcmd); + expect(encode(result.cells, result.language, { inline: true })).toEqual(srcmd); }); }); diff --git a/packages/api/types.mts b/packages/api/types.mts index 8fd8bce4..56c30305 100644 --- a/packages/api/types.mts +++ b/packages/api/types.mts @@ -1,4 +1,4 @@ -import type { CellType, SrcbookMetadataType } from '@srcbook/shared'; +import type { CellType, CodeLanguageType } from '@srcbook/shared'; export type SessionType = { id: string; @@ -7,7 +7,11 @@ export type SessionType = { */ dir: string; cells: CellType[]; - metadata: SrcbookMetadataType; + + /** + * The language of the srcbook, i.e.: 'typescript' or 'javascript' + */ + language: CodeLanguageType; /** * The tsconfig.json file contents. diff --git a/packages/shared/src/schemas/cells.ts b/packages/shared/src/schemas/cells.ts index 7aa69e43..992c7ebb 100644 --- a/packages/shared/src/schemas/cells.ts +++ b/packages/shared/src/schemas/cells.ts @@ -51,6 +51,10 @@ export const CellWithPlaceholderSchema = z.union([ PlaceholderCellSchema, ]); +// Used to parse metadata from a srcbook header in .src.md. +// +// i.e. +// export const SrcbookMetadataSchema = z.object({ language: z.enum(['javascript', 'typescript']), }); diff --git a/packages/web/src/components/settings-sheet.tsx b/packages/web/src/components/settings-sheet.tsx index 915e9e86..0b3f5fad 100644 --- a/packages/web/src/components/settings-sheet.tsx +++ b/packages/web/src/components/settings-sheet.tsx @@ -34,11 +34,11 @@ export function SettingsSheet({ session, open, onOpenChange, openDepsInstallModa
{title.text}

- {session.metadata.language === 'typescript' ? 'TypeScript' : 'JavaScript'} + {session.language === 'typescript' ? 'TypeScript' : 'JavaScript'}

- {session.metadata.language === 'typescript' && } + {session.language === 'typescript' && } diff --git a/packages/web/src/routes/home.tsx b/packages/web/src/routes/home.tsx index c430ee71..48cca6f3 100644 --- a/packages/web/src/routes/home.tsx +++ b/packages/web/src/routes/home.tsx @@ -126,7 +126,7 @@ export default function Home() { key={srcbook.id} title={(srcbook.cells[0] as TitleCellType).text} running={srcbook.cells.some((c) => c.type === 'code' && c.status === 'running')} - language={srcbook.metadata.language} + language={srcbook.language} cellCount={srcbook.cells.length} onClick={() => navigate(`/srcbooks/${srcbook.id}`)} onDelete={() => onDeleteSrcbook(srcbook)} diff --git a/packages/web/src/routes/session.tsx b/packages/web/src/routes/session.tsx index 85df93af..be549f08 100644 --- a/packages/web/src/routes/session.tsx +++ b/packages/web/src/routes/session.tsx @@ -46,14 +46,14 @@ function SessionPage() { useEffectOnce(() => { channel.subscribe(); - if (session.metadata.language === 'typescript') { + if (session.language === 'typescript') { channel.push('tsserver:start', { sessionId: session.id }); } return () => { channel.unsubscribe(); - if (session.metadata.language === 'typescript') { + if (session.language === 'typescript') { channel.push('tsserver:stop', { sessionId: session.id }); } }; @@ -152,7 +152,7 @@ function Session(props: { session: SessionType; channel: SessionChannel; config: let cell; switch (type) { case 'code': - cell = createCodeCell(index, session.metadata.language); + cell = createCodeCell(index, session.language); channel.push('cell:create', { sessionId: session.id, index, cell }); break; case 'markdown': @@ -172,7 +172,7 @@ function Session(props: { session: SessionType; channel: SessionChannel; config: let newCell; switch (cell.type) { case 'code': - newCell = createCodeCell(insertIdx, session.metadata.language, cell); + newCell = createCodeCell(insertIdx, session.language, cell); break; case 'markdown': newCell = createMarkdownCell(insertIdx, cell); @@ -213,7 +213,7 @@ function Session(props: { session: SessionType; channel: SessionChannel; config: {cells.map((cell, idx) => (
createNewCell('code', idx + 2)} createMarkdownCell={() => createNewCell('markdown', idx + 2)} createGenerateAiCodeCell={() => createNewCell('generate-ai', idx + 2)} @@ -252,7 +252,7 @@ function Session(props: { session: SessionType; channel: SessionChannel; config: {/* There is always an insert cell divider after the last cell */} createNewCell('code', allCells.length)} createMarkdownCell={() => createNewCell('markdown', allCells.length)} createGenerateAiCodeCell={() => createNewCell('generate-ai', allCells.length)} diff --git a/packages/web/src/types.ts b/packages/web/src/types.ts index 993cab3a..5fe7efec 100644 --- a/packages/web/src/types.ts +++ b/packages/web/src/types.ts @@ -1,4 +1,4 @@ -import { CellType, SrcbookMetadataType, CodeLanguageType } from '@srcbook/shared'; +import { CellType, CodeLanguageType } from '@srcbook/shared'; export interface FsObjectType { path: string; @@ -26,7 +26,7 @@ export type OutputType = StdoutOutputType | StderrOutputType; export type SessionType = { id: string; cells: CellType[]; - metadata: SrcbookMetadataType; + language: CodeLanguageType; // TODO: Better typing. // eslint-disable-next-line @typescript-eslint/no-explicit-any 'tsconfig.json'?: Record;