Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Read and write tsconfig to/from disk #160

Merged
merged 1 commit into from
Aug 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 13 additions & 5 deletions packages/api/ai/generate.mts
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,13 @@ const makeGenerateCellUserPrompt = (session: SessionType, insertIdx: number, que
text: '==== INTRODUCE CELL HERE ====',
});

const inlineSrcbookWithPlaceholder = encode(cellsWithPlaceholder, session.language, {
inline: true,
});
// Intentionally not passing in tsconfig.json here as that doesn't need to be in the prompt.
const inlineSrcbookWithPlaceholder = encode(
{ cells: cellsWithPlaceholder, language: session.language },
{
inline: true,
},
);

const prompt = `==== BEGIN SRCBOOK ====
${inlineSrcbookWithPlaceholder}
Expand All @@ -57,7 +61,11 @@ const makeGenerateCellEditUserPrompt = (
session: SessionType,
cell: CodeCellType,
) => {
const inlineSrcbook = encode(session.cells, session.language, { inline: true });
// Intentionally not passing in tsconfig.json here as that doesn't need to be in the prompt.
const inlineSrcbook = encode(
{ cells: session.cells, language: session.language },
{ inline: true },
);

const prompt = `==== BEGIN SRCBOOK ====
${inlineSrcbook}
Expand Down Expand Up @@ -150,7 +158,7 @@ export async function generateCells(
if (decodeResult.error) {
return { error: true, errors: decodeResult.errors };
} else {
return { error: false, cells: decodeResult.cells };
return { error: false, cells: decodeResult.srcbook.cells };
}
}

Expand Down
19 changes: 19 additions & 0 deletions packages/api/server/ws.mts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import type {
CellErrorType,
CellCreatePayloadType,
AiGenerateCellPayloadType,
TsConfigUpdatePayloadType,
} from '@srcbook/shared';
import {
CellErrorPayloadSchema,
Expand All @@ -50,6 +51,7 @@ import {
TsServerStopPayloadSchema,
TsServerCellDiagnosticsPayloadSchema,
CellCreatePayloadSchema,
TsConfigUpdatePayloadSchema,
} from '@srcbook/shared';
import tsservers from '../tsservers.mjs';
import { TsServer } from '../tsserver/tsserver.mjs';
Expand Down Expand Up @@ -568,6 +570,22 @@ async function tsserverStop(payload: TsServerStopPayloadType) {
tsservers.shutdown(payload.sessionId);
}

async function tsconfigUpdate(payload: TsConfigUpdatePayloadType) {
const session = await findSession(payload.sessionId);

if (!session) {
throw new Error(`No session exists for session '${payload.sessionId}'`);
}

const updatedSession = await updateSession(session, { 'tsconfig.json': payload.source });

if (tsservers.has(updatedSession.id)) {
const tsserver = tsservers.get(updatedSession.id);
tsserver.reloadProjects();
requestAllDiagnostics(tsserver, updatedSession);
}
}

wss
.channel('session:*')
.incoming('cell:exec', CellExecPayloadSchema, cellExec)
Expand All @@ -581,6 +599,7 @@ wss
.incoming('deps:validate', DepsValidatePayloadSchema, depsValidate)
.incoming('tsserver:start', TsServerStartPayloadSchema, tsserverStart)
.incoming('tsserver:stop', TsServerStopPayloadSchema, tsserverStop)
.incoming('tsconfig.json:update', TsConfigUpdatePayloadSchema, tsconfigUpdate)
.outgoing('cell:updated', CellUpdatedPayloadSchema)
.outgoing('cell:error', CellErrorPayloadSchema)
.outgoing('cell:output', CellOutputPayloadSchema)
Expand Down
24 changes: 17 additions & 7 deletions packages/api/session.mts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ import {
import { fileExists } from './fs-utils.mjs';
import { validFilename } from '@srcbook/shared';
import { pathToCodeFile } from './srcbook/path.mjs';
import { buildTsconfigJson } from './srcbook/config.mjs';

const sessions: Record<string, SessionType> = {};

Expand All @@ -52,17 +51,18 @@ export async function createSession(srcbookDir: string) {
throw new Error(`Cannot create session from invalid srcbook directory at ${srcbookDir}`);
}

const srcbook = result.srcbook;

const session: SessionType = {
id: Path.basename(srcbookDir),
dir: srcbookDir,
cells: result.cells,
language: result.language,
cells: srcbook.cells,
language: srcbook.language,
openedAt: Date.now(),
};

// TODO: Read from disk once we support editing tsconfig.json.
if (session.language === 'typescript') {
session['tsconfig.json'] = buildTsconfigJson();
session['tsconfig.json'] = srcbook['tsconfig.json'];
}

sessions[session.id] = session;
Expand Down Expand Up @@ -108,7 +108,7 @@ export async function updateSession(
const updatedSession = { ...session, ...updates };
sessions[id] = updatedSession;
if (flush) {
await writeToDisk(updatedSession.dir, session.language, updatedSession.cells);
await writeToDisk(updatedSession);
}
return updatedSession;
}
Expand All @@ -118,7 +118,17 @@ 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.language, { inline: true }));
return fs.writeFile(
destinationPath,
encode(
{
cells: session.cells,
language: session.language,
'tsconfig.json': session['tsconfig.json'],
},
{ inline: true },
),
);
}

export async function findSession(id: string): Promise<SessionType> {
Expand Down
64 changes: 47 additions & 17 deletions packages/api/srcbook/index.mts
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,9 @@ import { toFormattedJSON } from '../utils.mjs';
import { readdir } from '../fs-utils.mjs';
import { SRCBOOKS_DIR } from '../constants.mjs';
import { EXAMPLE_SRCBOOKS } from '../srcbook/examples.mjs';
import { pathToCodeFile, pathToPackageJson, pathToReadme } from './path.mjs';
import { pathToCodeFile, pathToPackageJson, pathToReadme, pathToTsconfigJson } from './path.mjs';
import { buildJSPackageJson, buildTSPackageJson, buildTsconfigJson } from './config.mjs';
import type { SessionType } from '../types.mjs';

function writeCellOnlyToDisk(srcbookDir: string, cell: PackageJsonCellType | CodeCellType) {
const path =
Expand All @@ -24,12 +25,22 @@ function writeCellOnlyToDisk(srcbookDir: string, cell: PackageJsonCellType | Cod
return fs.writeFile(path, cell.source, { encoding: 'utf8' });
}

export function writeToDisk(srcbookDir: string, language: CodeLanguageType, cells: CellType[]) {
const writes = [writeReadmeToDisk(srcbookDir, language, cells)];
export function writeToDisk(
srcbook: Pick<SessionType, 'dir' | 'cells' | 'language' | 'tsconfig.json'>,
) {
const writes = [writeReadmeToDisk(srcbook.dir, srcbook.language, srcbook.cells)];

if (srcbook['tsconfig.json']) {
writes.push(
fs.writeFile(pathToTsconfigJson(srcbook.dir), srcbook['tsconfig.json'], {
encoding: 'utf8',
}),
);
}

for (const cell of cells) {
for (const cell of srcbook.cells) {
if (cell.type === 'package.json' || cell.type === 'code') {
writes.push(writeCellOnlyToDisk(srcbookDir, cell));
writes.push(writeCellOnlyToDisk(srcbook.dir, cell));
}
}

Expand Down Expand Up @@ -68,7 +79,7 @@ export function writeReadmeToDisk(
language: CodeLanguageType,
cells: CellType[],
) {
return fs.writeFile(pathToReadme(srcbookDir), encode(cells, language, { inline: false }), {
return fs.writeFile(pathToReadme(srcbookDir), encode({ cells, language }, { inline: false }), {
encoding: 'utf8',
});
}
Expand Down Expand Up @@ -106,9 +117,25 @@ export async function importSrcbookFromSrcmdText(text: string, directoryBasename
throw new Error(`Cannot decode invalid srcmd`);
}

const dirname = await createSrcbookDir(result.language, directoryBasename);
const srcbook = result.srcbook;

const dirname = await createSrcbookDir(directoryBasename);

await writeToDisk(dirname, result.language, result.cells);
if (srcbook.language === 'typescript') {
// It's possible the srcmd text does not contain tsconfig.json contents.
// If that's the case, we must generate a new tsconfig.json file with our defaults
// because reading from this directory will fail if tsconfig.json is missing.
const tsconfig = srcbook['tsconfig.json'] || toFormattedJSON(buildTsconfigJson());

await writeToDisk({
dir: dirname,
cells: srcbook.cells,
language: srcbook.language,
'tsconfig.json': tsconfig,
});
} else {
await writeToDisk({ dir: dirname, ...srcbook });
}

return dirname;
}
Expand All @@ -120,7 +147,7 @@ export async function importSrcbookFromSrcmdText(text: string, directoryBasename
* Users are not supposed to be aware or modify private directories.
*/
export async function createSrcbook(title: string, language: CodeLanguageType) {
const dirname = await createSrcbookDir(language);
const dirname = await createSrcbookDir();

const cells: CellType[] = [
{
Expand All @@ -137,12 +164,21 @@ export async function createSrcbook(title: string, language: CodeLanguageType) {
},
];

await writeToDisk(dirname, language, cells);
if (language === 'typescript') {
await writeToDisk({
dir: dirname,
language,
cells,
'tsconfig.json': toFormattedJSON(buildTsconfigJson()),
});
} else {
await writeToDisk({ dir: dirname, language, cells });
}

return dirname;
}

async function createSrcbookDir(language: CodeLanguageType, basename: string = randomid()) {
async function createSrcbookDir(basename: string = randomid()) {
const srcbookDirectoryPath = Path.join(SRCBOOKS_DIR, basename);

// Create the srcbook directory
Expand All @@ -152,12 +188,6 @@ async function createSrcbookDir(language: CodeLanguageType, basename: string = r
const srcPath = Path.join(srcbookDirectoryPath, 'src');
await fs.mkdir(srcPath);

// Create the tsconfig.json file for typescript projects
if (language === 'typescript') {
const tsconfigPath = Path.join(srcbookDirectoryPath, 'tsconfig.json');
await fs.writeFile(tsconfigPath, toFormattedJSON(buildTsconfigJson()), { encoding: 'utf8' });
}

return srcbookDirectoryPath;
}

Expand Down
21 changes: 18 additions & 3 deletions packages/api/srcmd.mts
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import fs from 'node:fs/promises';
import { pathToCodeFile, pathToPackageJson, pathToReadme } from './srcbook/path.mjs';
import {
pathToCodeFile,
pathToPackageJson,
pathToReadme,
pathToTsconfigJson,
} from './srcbook/path.mjs';
import type { DecodeResult } from './srcmd/types.mjs';

import { encode } from './srcmd/encoding.mjs';
Expand Down Expand Up @@ -30,7 +35,9 @@ export async function decodeDir(dir: string): Promise<DecodeResult> {
return readmeResult;
}

const cells = readmeResult.cells;
const srcbook = readmeResult.srcbook;

const cells = srcbook.cells;
const pendingFileReads: Promise<void>[] = [];

// Let's replace all the code cells with the actual file contents for each one
Expand All @@ -52,7 +59,15 @@ export async function decodeDir(dir: string): Promise<DecodeResult> {
// Wait for all file reads to complete
await Promise.all(pendingFileReads);

return { error: false, language: readmeResult.language, cells };
if (srcbook.language === 'typescript') {
const tsconfig = await fs.readFile(pathToTsconfigJson(dir), 'utf8');
return {
error: false,
srcbook: { language: srcbook.language, cells, 'tsconfig.json': tsconfig },
};
} else {
return { error: false, srcbook: { language: srcbook.language, cells } };
}
} catch (e) {
const error = e as unknown as Error;
return { error: true, errors: [error.message] };
Expand Down
11 changes: 9 additions & 2 deletions packages/api/srcmd/decoding.mts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,14 @@ 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, language: metadata.language, cells: convertToCells(groups) };
: {
error: false,
srcbook: {
language: metadata.language,
cells: convertToCells(groups),
'tsconfig.json': metadata['tsconfig.json'],
},
};
}

/**
Expand All @@ -58,7 +65,7 @@ export function decodeCells(contents: string): DecodeCellsResult {
const errors = validateTokenGroupsPartial(groups);
return errors.length > 0
? { error: true, errors }
: { error: false, cells: convertToCells(groups) };
: { error: false, srcbook: { cells: convertToCells(groups) } };
}

const SRCBOOK_METADATA_RE = /^<!--\s*srcbook:(.+)\s*-->$/;
Expand Down
23 changes: 14 additions & 9 deletions packages/api/srcmd/encoding.mts
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,28 @@ import type {
MarkdownCellType,
PackageJsonCellType,
TitleCellType,
CellWithPlaceholderType,
PlaceholderCellType,
CodeLanguageType,
CellWithPlaceholderType,
} from '@srcbook/shared';
import type { SrcbookType } from './types.mjs';

export function encode(
allCells: CellWithPlaceholderType[],
language: CodeLanguageType,
options: { inline: boolean },
) {
const [firstCell, secondCell, ...remainingCells] = allCells;
type SrcbookWithPlacebolderType = Omit<SrcbookType, 'cells'> & {
cells: CellWithPlaceholderType[];
};

export function encode(srcbook: SrcbookWithPlacebolderType, options: { inline: boolean }) {
const [firstCell, secondCell, ...remainingCells] = srcbook.cells;
const titleCell = firstCell as TitleCellType;
const packageJsonCell = secondCell as PackageJsonCellType;
const cells = remainingCells as (MarkdownCellType | CodeCellType | PlaceholderCellType)[];

const metadata =
srcbook.language === 'javascript'
? { language: srcbook.language }
: { language: srcbook.language, 'tsconfig.json': srcbook['tsconfig.json'] };

const encoded = [
`<!-- srcbook:${JSON.stringify({ language })} -->`,
`<!-- srcbook:${JSON.stringify(metadata)} -->`,
encodeTitleCell(titleCell),
encodePackageJsonCell(packageJsonCell, options),
...cells.map((cell) => {
Expand Down
14 changes: 10 additions & 4 deletions packages/api/srcmd/types.mts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { type CellType, type CodeLanguageType } from '@srcbook/shared';
import type { SessionType } from '../types.mjs';

export type SrcbookType = Pick<SessionType, 'cells' | 'language' | 'tsconfig.json'>;

export type DecodeErrorResult = {
error: true;
Expand All @@ -7,12 +9,16 @@ export type DecodeErrorResult = {

export type DecodeSuccessResult = {
error: false;
cells: CellType[];
language: CodeLanguageType;
srcbook: SrcbookType;
};

export type DecodeCellsSuccessResult = {
error: false;
srcbook: Pick<SessionType, 'cells'>;
};

// 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<DecodeSuccessResult, 'language'>;
export type DecodeCellsResult = DecodeErrorResult | DecodeCellsSuccessResult;
Loading
Loading