Repository navigation
feat: import a folder as a project #210
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
Merged
Merged
Changes from all commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
9a45061
refactor(schemas): share Builder's app file rules
brianjfox e662743
feat(schemas): read a directory as a project file
brianjfox a5ec0ee
feat(hub): have the coordinator write the documents an imported app l…
brianjfox 42659e2
feat(web): import a folder as a project
brianjfox 357b3fb
feat(scripts): import a repository from the command line
brianjfox 6748479
Merge branch 'feat/no-app-size-limit' into feat/import-folder
brianjfox 5627b52
refactor: drop invented fallbacks, nested ternaries and promise chain…
brianjfox File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or 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 hidden or 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 hidden or 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 hidden or 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 hidden or 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 hidden or 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,47 @@ | ||
| import { expect, test } from "bun:test"; | ||
| import { describeBytes, listedDirectory, readDirectory } from "./repository.ts"; | ||
|
|
||
| const listed = (path: string, text: string) => { | ||
| const blob = new Blob([text]); | ||
| return { | ||
| name: path.slice(path.lastIndexOf("/") + 1), | ||
| size: blob.size, | ||
| webkitRelativePath: path, | ||
| arrayBuffer: () => blob.arrayBuffer(), | ||
| }; | ||
| }; | ||
|
|
||
| test("a folder input's files lose the folder's name and never include dependencies", async () => { | ||
| const picked = listedDirectory([ | ||
| listed("rtv7/package.json", '{"name":"rtv7"}'), | ||
| listed("rtv7/src/main.ts", "export {};"), | ||
| listed("rtv7/node_modules/left/index.js", ""), | ||
| listed("rtv7/.git/HEAD", "ref: refs/heads/main"), | ||
| ])!; | ||
| expect(picked.name).toBe("rtv7"); | ||
| expect(picked.files.map((file) => file.path)).toEqual([ | ||
| "package.json", | ||
| "src/main.ts", | ||
| ]); | ||
| const { file, report } = await readDirectory(picked, new Set()); | ||
| expect(report.title).toBe("Rtv7"); | ||
| expect(file.documents.map((document) => document.kind)).toEqual(["app"]); | ||
| expect(report.missing).toEqual([ | ||
| "problem_brief", | ||
| "constraints", | ||
| "approaches", | ||
| "product_requirements", | ||
| "design", | ||
| "build_plan", | ||
| ]); | ||
| }); | ||
|
|
||
| test("an empty listing is no folder", () => { | ||
| expect(listedDirectory([])).toBeUndefined(); | ||
| }); | ||
|
|
||
| test("sizes read as people do", () => { | ||
| expect(describeBytes(512)).toBe("512 B"); | ||
| expect(describeBytes(20 * 1024)).toBe("20 KB"); | ||
| expect(describeBytes(1.5 * 1024 * 1024)).toBe("1.5 MB"); | ||
| }); |
This file contains hidden or 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,101 @@ | ||
| import { EXCLUDED_DIRECTORIES } from "@corbits-builder/schemas/app-files"; | ||
| import { | ||
| readRepository, | ||
| type RepositoryFile, | ||
| type RepositoryImport, | ||
| } from "@corbits-builder/schemas/repository-import"; | ||
|
|
||
| /** A folder the person picked: its name and its files, read lazily. */ | ||
| export type PickedDirectory = { name: string; files: RepositoryFile[] }; | ||
|
|
||
| // The File System Access API, which the DOM typings leave out: Chromium's folder picker hands | ||
| // over a handle the page walks itself, so node_modules and .git are never listed. | ||
| type FileHandle = { kind: "file"; name: string; getFile(): Promise<File> }; | ||
| type DirectoryHandle = { | ||
| kind: "directory"; | ||
| name: string; | ||
| values(): AsyncIterable<FileHandle | DirectoryHandle>; | ||
| }; | ||
| type DirectoryPicker = (options: { mode: "read" }) => Promise<DirectoryHandle>; | ||
|
|
||
| const directoryPicker = () => | ||
| (window as { showDirectoryPicker?: DirectoryPicker }).showDirectoryPicker; | ||
|
|
||
| /** Whether the browser has a folder picker; otherwise a folder input lists the files. */ | ||
| export const canPickDirectory = () => directoryPicker() !== undefined; | ||
|
|
||
| const lazily = (file: File, path: string): RepositoryFile => ({ | ||
| path, | ||
| size: file.size, | ||
| bytes: async () => new Uint8Array(await file.arrayBuffer()), | ||
| }); | ||
|
|
||
| async function walk( | ||
| handle: DirectoryHandle, | ||
| prefix: string, | ||
| ): Promise<RepositoryFile[]> { | ||
| const files: RepositoryFile[] = []; | ||
| for await (const entry of handle.values()) { | ||
| if (entry.kind === "directory") { | ||
| if (!EXCLUDED_DIRECTORIES.has(entry.name)) | ||
| files.push(...(await walk(entry, `${prefix}${entry.name}/`))); | ||
| } else files.push(lazily(await entry.getFile(), prefix + entry.name)); | ||
| } | ||
| return files; | ||
| } | ||
|
|
||
| /** Opens the folder picker; undefined when the person closes it without choosing. */ | ||
| export async function pickDirectory(): Promise<PickedDirectory | undefined> { | ||
| const picker = directoryPicker(); | ||
| if (!picker) return undefined; | ||
| try { | ||
| const handle = await picker({ mode: "read" }); | ||
| return { name: handle.name, files: await walk(handle, "") }; | ||
| } catch (cause) { | ||
| if (cause instanceof DOMException && cause.name === "AbortError") | ||
| return undefined; | ||
| throw cause; | ||
| } | ||
| } | ||
|
|
||
| /** What a folder input lists: each file under the chosen folder's name. */ | ||
| export type ListedFile = { | ||
| name: string; | ||
| size: number; | ||
| webkitRelativePath: string; | ||
| arrayBuffer(): Promise<ArrayBuffer>; | ||
| }; | ||
|
|
||
| /** | ||
| * The files a folder input lists, with the chosen folder's name taken off each path and the | ||
| * directories the app never keeps left out. | ||
| */ | ||
| export function listedDirectory( | ||
| listed: Iterable<ListedFile>, | ||
| ): PickedDirectory | undefined { | ||
| let name: string | undefined; | ||
| const files: RepositoryFile[] = []; | ||
| for (const file of listed) { | ||
| const parts = file.webkitRelativePath.split("/"); | ||
| name ??= parts[0]; | ||
| const path = parts.slice(1); | ||
| if (path.length === 0) continue; | ||
| if (path.some((part) => EXCLUDED_DIRECTORIES.has(part))) continue; | ||
| files.push(lazily(file as unknown as File, path.join("/"))); | ||
| } | ||
| return name === undefined ? undefined : { name, files }; | ||
| } | ||
|
|
||
| /** Reads a picked folder as a project, leaving out the paths the person unticked. */ | ||
| export const readDirectory = ( | ||
| picked: PickedDirectory, | ||
| exclude: ReadonlySet<string>, | ||
| ): Promise<RepositoryImport> => | ||
| readRepository(picked.files, { name: picked.name, exclude }); | ||
|
|
||
| /** A size as a person reads it. */ | ||
| export function describeBytes(bytes: number): string { | ||
| if (bytes < 1024) return `${bytes} B`; | ||
| if (bytes < 1024 * 1024) return `${Math.round(bytes / 1024)} KB`; | ||
| return `${(bytes / (1024 * 1024)).toFixed(1)} MB`; | ||
| } |
This file contains hidden or 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 hidden or 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
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
P2 · Opening message always tells the coordinator to write the brief, even when the brief was imported
The message lists only the missing kinds, but its instruction still says to write the brief and then have the writers produce the rest. If the import already includes problem_brief and lacks only later documents such as design or build_plan, the coordinator is told to write a brief anyway. That can replace or duplicate the person's imported brief and send them through approvals they don't need.
Import a folder whose files include the app and a problem_brief but no design or build_plan.
Resolve this comment. Triage will not finish while it is open.