A zero-backend, browser-native project scaffolding tool with a VS Code-style interface — paste an ASCII folder tree or build one manually, edit file contents in a multi-tab code editor, drag and drop nodes to reorganize, right-click for a context menu, and export a real, fully populated ZIP file using JSZip. No server, no CLI, instant download.

Every time I start a new project, I spend the first 20 minutes doing the same thing: creating folders, touching files, writing boilerplate. For projects where I already know the structure upfront — or when I want to scaffold something from an ASCII tree I found in a README — there's no good browser-based tool that lets you go from structure to downloadable ZIP in one step.
StructureGen is that tool. It's a browser-native, VS Code-inspired project scaffolding environment where you can paste an ASCII folder tree, build a structure manually, edit file contents in a tabbed code editor, reorganize nodes with drag and drop, and export a real, fully populated ZIP file — all without installing anything or leaving the browser.
StructureGen has three layers that work together: a file explorer sidebar, a multi-tab code editor, and an import/export system.
tree command output (with ├──, └──, │ characters) into the import modal and the tool parses it into a live, interactive node tree. Depth is inferred from leading whitespace and tree drawing characters.<textarea> with Tab key support (inserts 2 spaces instead of moving focus), file path breadcrumb, and language label.package.json gets a full JSON scaffold. next.config.js, tailwind.config.js, postcss.config.js, and middleware.js get their standard boilerplate. .env files get a comment header. .gitignore gets common ignore patterns. JS/TS files get a default export component stub. HTML files get a full DOCTYPE skeleton..zip download named after the root folder. Folders are explicitly created in the ZIP so empty directories are preserved.'use client' component — no SSR, no server actions.useState, useEffect, useCallback, useRef. No external state management. The entire node tree is stored as a flat nodeMap (id → node object) plus a rootIds array — a normalized structure that makes insertions, deletions, and moves O(1) without deep cloning.import('jszip'). This keeps the initial bundle lean — JSZip (~100KB) is never loaded unless needed.dragstart, dragover, dragleave, drop, dragend) drive the tree reorganization system.The entire file system is represented as a flat normalized map — not a nested tree. This is the most important architectural decision in the project.
// Each node in the nodeMap:
{
id: 'n42',
name: 'components',
isFolder: true,
parentId: 'n7', // null for root nodes
children: ['n43','n44'], // only for folders
content: '' // only for files
}
// rootIds: string[] — ordered list of top-level node IDs
// nodeMap: Record — all nodes indexed by ID
A flat map means inserting a node into the middle of a sibling list, moving a node to a different parent, or deleting a subtree are all done by updating a few array entries and object references — no recursive tree mutation, no deep cloning on every operation. The tradeoff is that rendering the tree requires a recursive TreeNode component that looks up children by ID, but React handles this efficiently with stable keys.
The parseStructure function converts pasted ASCII tree text into the flat nodeMap + rootIds representation. It processes the input line by line, using indentation depth to determine parent-child relationships:
for (const line of lines) {
// Strip tree-drawing chars (├ └ ─ │) to get clean indentation
const cleaned = line.replace(/[├└]/g, ' ').replace(/─/g, ' ').replace(/│/g, ' ')
const depth = Math.floor(leading / 4)
const name = line.replace(/[│├└─]/g, '').trim()
const isFolder = name.endsWith('/')
// Stack tracks the current ancestor chain by depth
while (stack.length && stack[stack.length - 1].depth >= depth) stack.pop()
const parentId = stack.length ? stack[stack.length - 1].id : null
const node = makeNode(cleanName, isFolder, parentId)
if (isFolder) stack.push({ id: node.id, depth })
}
The stack is the key — it maintains the current ancestor chain. When a line is shallower than the previous one, ancestors are popped until the stack matches the current depth. This handles arbitrary nesting without recursion and correctly processes the tree in a single O(n) pass.
The drag and drop system uses native HTML5 drag events wired up on each TreeNode row. The dragged node's ID is tracked in a ref (not state) to avoid the stale closure problem in drag event handlers — state updates in dragstart don't propagate to dragover handlers that closed over the initial value.
// Drop position determined by cursor Y within the target row
const y = e.clientY - rect.top
const h = rect.height
if (isFolder && y > h * 0.25 && y < h * 0.75) pos = 'inside'
else if (y <= h * 0.5) pos = 'before'
else pos = 'after'
The moveNode function handles all three cases: moving inside a folder (change parentId, append to folder's children), moving before a sibling (find target index in parent's children array, splice at that index), moving after a sibling (splice at index + 1). It also guards against invalid moves — dropping a node onto itself or onto one of its own descendants — using the isDescendant helper that walks up the parent chain.
The tab system mirrors how VS Code manages open files. Each tab is a { id, nodeId } object stored in the openTabs array, with activeTabId tracking which tab is focused. Opening a file that's already in a tab activates that tab rather than opening a duplicate. Closing a tab activates the tab to the left, or the first remaining tab if the closed tab was first. If no tabs remain, the editor pane shows the "select a file" empty state.
Tab content is kept in the nodeMap rather than in the tab objects — so edits to a file persist even after the tab is closed and reopened. The <textarea> uses defaultValue (not value) with a key={activeTab.nodeId} to reset the DOM value when switching between files, avoiding stale content rendering without triggering a controlled input re-render on every keystroke.
The export pipeline is triggered by a single button click and runs entirely client-side:
const JSZip = (await import('jszip')).default // loaded on demand
const zip = new JSZip()
Object.values(nodeMap).forEach(node => {
const path = getFullPath(node.id, nodeMap) // reconstruct full path
node.isFolder
? zip.folder(path)
: zip.file(path, node.content || '')
})
const blob = await zip.generateAsync({ type: 'blob', compression: 'DEFLATE' })
// → trigger download
The getFullPath function reconstructs the full path for any node by recursively walking up the parentId chain and joining names with /. Empty folders are added explicitly with zip.folder(path) so they appear in the ZIP even with no files inside. The ZIP is named after the root node — so if your tree starts with crewera-next/, the download is crewera-next.zip.
One non-obvious challenge with generating ZIP files containing .js files client-side is that Windows SmartScreen may block or warn about extracted JavaScript files from an unrecognized source. The tool displays a persistent notice at the bottom of the page with two fixes: right-click the ZIP → Properties → Unblock, or extract with 7-Zip or WinRAR. This is a real friction point for Windows users and worth surfacing prominently rather than leaving them to debug it themselves.
<textarea> with Tab key support. Replacing it with Monaco (the engine behind VS Code) would add real syntax highlighting, auto-indentation, bracket matching, and IntelliSense — making the file editing experience genuinely useful rather than functional.nodeMap and rootIds to localStorage on every change would let users pick up where they left off without re-importing.StructureGen started as a personal frustration — I kept wanting to go from an ASCII tree to a real folder structure without opening a terminal. Building it taught me a lot about normalized state design, the subtleties of the HTML5 drag and drop API (especially the stale ref problem in drag handlers), how to parse structured text with a stack-based algorithm, and the practical limits of <textarea> as a code editor. The tool is genuinely useful in my own workflow, which is the best kind of project to build.
// Tech Stack