Skip to content

feat(import): add design.md import reverse command#40

Open
zagi wants to merge 2 commits intogoogle-labs-code:mainfrom
zagi:feat/reverse_import_with_auto_detection
Open

feat(import): add design.md import reverse command#40
zagi wants to merge 2 commits intogoogle-labs-code:mainfrom
zagi:feat/reverse_import_with_auto_detection

Conversation

@zagi
Copy link
Copy Markdown

@zagi zagi commented Apr 23, 2026

Generates a DESIGN.md from an existing Node.js project by statically
analyzing its design sources. No AI, no network — deterministic code
analysis that runs in ~5ms on a clean project.

Pipeline

  detect framework → scan sources → parse → merge → emit

What it reads

  • package.json / README.md — project name, description, version, and first-paragraph intro. README H1 beats package.json.name; directory basename is the final fallback.
  • Tailwind configs (tailwind.config.{js,ts,cjs,mjs}) — loaded via dynamic import (Bun handles TS natively); theme.extend is walked for colors, borderRadius, spacing, fontSize (incl. the [size, meta] tuple form), and fontFamily. Regex fallback on eval errors so malformed configs still surface their color block.
  • CSS custom properties — both Tailwind v4 @theme { } blocks (with prefix-stripping: --color-primarycolors.primary, --spacing-md, --radius-lg, --font-*, --text-*, --leading-*, --tracking-*, --font-weight-*; --breakpoint-* skipped) and legacy :root { } blocks (name-heuristic classification).
  • DTCG tokens (tokens.json, design-tokens.json, design_tokens.json, *.tokens.json) — walks $type/$value; only accepts tokens under colors / spacing / rounded / typography top-level sections so per-component dimensions don't pollute the scale.

Framework detection

Cosmetic, reported in the UI. Recognizes Next, Nuxt, Vite, SvelteKit,
Remix, Astro, Create React App, Gatsby, Angular, Vue CLI, and falls
back to generic Node / unknown. Meta-frameworks beat Vite on conflicts.

Scan hygiene

Bounded at depth 5. Skips node_modules, .git, .next, .nuxt, .output,
.svelte-kit, .turbo, build, coverage, dist. Also skips vendor trees
(public, static, vendor, vendors, third-party, third_party, bundles,
charting_library), minified/RTL stylesheets (*.min.css, .rtl.css),
and hashed bundler output (..css) — so e.g. a bundled
TradingView charting library's 40+ v-rhythm-
tokens don't leak into
the project's own design system.

Merge

Precedence: CSS → Tailwind → DTCG (later wins because DTCG is most
structured). Rebuilds the flat symbolTable the linter expects so the
generated state can be round-tripped through lint/export.

Output

YAML frontmatter (name, description, colors, typography, rounded,
spacing) plus a markdown body: # Name heading, description, README
intro, ## Overview (framework + counts + source summary),
per-section bullet lists of the imported tokens, and a footer
inviting the team to edit the prose. The frontmatter alone
round-trips cleanly through lint and back through export.

CLI

  design.md import <project>              # writes <project>/DESIGN.md
  design.md import <project> --dryRun     # prints to stdout
  design.md import <project> --format json # NDJSON progress events

Pretty mode renders live via Ink, showing staged progress (◐/✓/⚠/✗)
for detect → scan → parse → merge → write. JSON mode emits one
ImportStep per line on stdout for scripts and CI.

Tests

275 passing. Unit tests cover every parser, the framework detector,
the source scanner's vendor filtering, the merger, the markdown
emitter, project metadata, and the Ink component. Integration tests
(VR-1, VR-2) round-trip examples/paws-and-paths, atmospheric-glass,
totality-festival, and three framework fixtures (Next, Vite, Nuxt)
through import → lint and assert zero linter errors.

Build

Marks ink, react, and react-devtools-core as --external so Ink 7's
devtools import doesn't break the bundler.

  Generates a DESIGN.md from an existing Node.js project by statically
  analyzing its design sources. No AI, no network — deterministic code
  analysis that runs in ~5ms on a clean project.

  ## Pipeline

      detect framework → scan sources → parse → merge → emit

  ## What it reads

  - **package.json / README.md** — project name, description, version,
    and first-paragraph intro. README H1 beats package.json.name;
    directory basename is the final fallback.
  - **Tailwind configs** (`tailwind.config.{js,ts,cjs,mjs}`) — loaded
    via dynamic import (Bun handles TS natively); `theme.extend` is
    walked for colors, borderRadius, spacing, fontSize (incl. the
    `[size, meta]` tuple form), and fontFamily. Regex fallback on
    eval errors so malformed configs still surface their color block.
  - **CSS custom properties** — both Tailwind v4 `@theme { }` blocks
    (with prefix-stripping: `--color-primary` → `colors.primary`,
    `--spacing-md`, `--radius-lg`, `--font-*`, `--text-*`, `--leading-*`,
    `--tracking-*`, `--font-weight-*`; `--breakpoint-*` skipped) and
    legacy `:root { }` blocks (name-heuristic classification).
  - **DTCG tokens** (`tokens.json`, `design-tokens.json`,
    `design_tokens.json`, `*.tokens.json`) — walks `$type`/`$value`;
    only accepts tokens under `colors` / `spacing` / `rounded` /
    `typography` top-level sections so per-component dimensions don't
    pollute the scale.

  ## Framework detection

  Cosmetic, reported in the UI. Recognizes Next, Nuxt, Vite, SvelteKit,
  Remix, Astro, Create React App, Gatsby, Angular, Vue CLI, and falls
  back to generic Node / unknown. Meta-frameworks beat Vite on conflicts.

  ## Scan hygiene

  Bounded at depth 5. Skips node_modules, .git, .next, .nuxt, .output,
  .svelte-kit, .turbo, build, coverage, dist. Also skips vendor trees
  (public, static, vendor, vendors, third-party, third_party, bundles,
  charting_library), minified/RTL stylesheets (*.min.css, *.rtl.css),
  and hashed bundler output (<name>.<hash>.css) — so e.g. a bundled
  TradingView charting library's 40+ v-rhythm-* tokens don't leak into
  the project's own design system.

  ## Merge

  Precedence: CSS → Tailwind → DTCG (later wins because DTCG is most
  structured). Rebuilds the flat symbolTable the linter expects so the
  generated state can be round-tripped through lint/export.

  ## Output

  YAML frontmatter (name, description, colors, typography, rounded,
  spacing) plus a markdown body: `# Name` heading, description, README
  intro, `## Overview` (framework + counts + source summary),
  per-section bullet lists of the imported tokens, and a footer
  inviting the team to edit the prose. The frontmatter alone
  round-trips cleanly through `lint` and back through `export`.

  ## CLI

      design.md import <project>              # writes <project>/DESIGN.md
      design.md import <project> --dryRun     # prints to stdout
      design.md import <project> --format json # NDJSON progress events

  Pretty mode renders live via Ink, showing staged progress (◐/✓/⚠/✗)
  for detect → scan → parse → merge → write. JSON mode emits one
  ImportStep per line on stdout for scripts and CI.

  ## Tests

  275 passing. Unit tests cover every parser, the framework detector,
  the source scanner's vendor filtering, the merger, the markdown
  emitter, project metadata, and the Ink component. Integration tests
  (VR-1, VR-2) round-trip examples/paws-and-paths, atmospheric-glass,
  totality-festival, and three framework fixtures (Next, Vite, Nuxt)
  through import → lint and assert zero linter errors.

  ## Build

  Marks ink, react, and react-devtools-core as --external so Ink 7's
  devtools import doesn't break the bundler.
@google-cla
Copy link
Copy Markdown

google-cla Bot commented Apr 23, 2026

Thanks for your pull request! It looks like this may be your first contribution to a Google open source project. Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA).

View this failed invocation of the CLA check for more information.

For the most up to date status, view the checks section at the bottom of the pull request.

  Threat model: user runs `design.md import` on a repo they don't fully
  trust. Every file under the project root is attacker-controlled.

  - safe-eval: replace dynamic import() with vm.runInNewContext + inert-
    clone of exports (strips getters/functions/proxies) to block the
    Error.prepareStackTrace realm escape. ReDoS-proof TS type stripping
    (linear-time negated class; old `as` regex was O(n²), 28s on 50k
    stacked casts).
  - safe-write: O_NOFOLLOW + lstat + realpath containment so a planted
    `DESIGN.md -> ~/.zshrc` symlink cannot redirect the write.
  - source-scanner: lstatSync + skip symlinks so an `evil.tokens.json
    -> /etc/passwd` symlink cannot exfiltrate host files into DESIGN.md.
  - safe-json: JSON.parse reviver drops __proto__/constructor/prototype;
    framework-detector builds a null-prototype deps map.
  - markdown-emitter: sanitize description/README intro (collapse
    newlines, escape HTML and leading `#`) and wrap README intro in a
    blockquote so downstream LLM consumers attribute it to the repo.
  - error-sanitize: stderr defaults to {code}-only; --verbose opts into
    a path-redacted message. Redaction handles unicode, spaces, and
    URLs without over-matching.
  - runImport canonicalizes projectPath via realpathSync once and uses
    the same root for scan + write containment (no TOCTOU split).

  Red-team verified: getter RCE, symlink overwrite, symlink scan escape,
  and __proto__ pollution all blocked in one malicious repo. 319 tests.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant