Skip to content

feat: Vue-to-Studio importer — first-class import of frappe app pages#179

Draft
rmehta wants to merge 1 commit into
frappe:developfrom
rmehta:develop
Draft

feat: Vue-to-Studio importer — first-class import of frappe app pages#179
rmehta wants to merge 1 commit into
frappe:developfrom
rmehta:develop

Conversation

@rmehta
Copy link
Copy Markdown
Member

@rmehta rmehta commented Jun 5, 2026

Original prompt

i want to edit the frappe/crm frontend using frappe/studio. both these apps are installed on my local test-bench. my strategy is to write an importer that will build the frappe/studio object model by reading and building Vue source files from the target app, say frappe/crm. map pages, and components. the importer sees standard frappe-ui components it will use the studio defaults, if it sees composite Vue components it will add them as custom components in studio. i should be able to populate the studio database from their equivalent vue file mapping. my goal is to start with frappe/crm. make plan to design this importer.

What this does

Adds a pipeline that reads Vue SFC files from an installed frappe app's frontend and imports them as Studio Pages + Studio Components — no manual rebuild or mapping required. Tested end-to-end with CRM (16 pages, 91 components).

Follow-up prompts that drove the rendering fixes

"can you check if the pages render correctly in the studio editor, i am getting an error when i open a page"

"check for errors on the js console" → SyntaxError: Unexpected end of JSON input at codeStore.ts / helpers.ts

"now i don't see a JS error, but the canvas is empty"

"i can see some standard components being rendered now, but custom components are not being rendered. i am checking the layoutheader component in the notes page and the component seems empty."

"the components seem mapped, but their inner values and text is not"

"also ensure styles and custom tokens are mapped"

"instead of text i see 'text-block', check if text is captured in attributes, also i am looking at the viewbreadcrumbs object, its attributes (slots?, props?) don't map, can we try and make the breadcrumbs appear in the notes editor page as we expect?"

What's included

Node.js parser (tools/vue-importer/)

  • router-parser.js — discovers page routes from the app's Vue Router config
  • classifier.js — maps Vue/HTML tag names to Studio component names
  • template-walker.js — converts Vue template AST to Studio block trees (containers, text nodes, v-if, v-for, Teleport, named slots, classes, inline styles)
  • script-analyzer.js — extracts createResource calls, ref variables, watch watchers, defineProps/defineModel props

Python importer (studio/importer.py)

  • Invokes the Node parser via subprocess, receives a JSON manifest
  • Upserts Studio App, Studio Pages and Studio Components
  • JSON-encodes String variables so codeStore's JSON.parse doesn't crash
  • Studio Import Log doctype tracks status, counts and errors

API & UI

  • Three whitelisted endpoints: get_importable_apps, preview_import, start_import
  • ImportPanel.vue — "Import" tab in the left panel with app listing, page/component checkboxes, and progress

Rendering bugs fixed during CRM testing

Bug Fix
JSON.parse crash on empty variable initial values json.dumps() for String type; "null" for empty Object
Empty canvas — canHaveChildren() always false Root block gets componentId:"root" + originalElement:"div"
Custom components rendering empty (Teleport/slot) Teleport → transparent pass-through; slot → render fallback; isComponentContext flag suppresses v-if conditions referencing component-local state
Text content missing walkNode handles TEXT and INTERPOLATION node types; extractTextContent collects compound expression parts
Tailwind classes not captured extractClasses also handles :class="[...]" dynamic bindings
Inline styles not mapped extractInlineStyles parses CSS strings into camelCase property objects
Component prop expressions not resolving in Studio Template walker rewrites {{ routeName }} → {{ inputs.routeName }} for component templates, matching Studio's { inputs: {...} } evaluation context; defineModel variables are mapped to modelValue
componentName case mismatch Custom component names converted to kebab-case to match componentStore cache keys

Plan / what's next

  • Handle v-for repeaters — currently stubbed as Repeater blocks but dataSource mapping needs work
  • Map more frappe-ui components (currently unmapped ones fall back to Container)
  • __() translation calls in expressions — available in globalContext?
  • Test with other CRM-family apps (HRMS, Helpdesk)
  • UI polish: import progress streaming, per-page preview in panel
  • Consider incremental re-import (diff blocks rather than full replace)

Adds a complete pipeline that reads Vue SFC files from an installed
frappe app's frontend and imports them as Studio Pages, Studio
Components, variables, resources and watchers — no manual rebuild
required.

## What's included

### Node.js parser (`tools/vue-importer/`)
- `router-parser.js` — walks the app's Vue Router config to discover
  page routes and their source files
- `classifier.js` — maps Vue/HTML tag names to Studio component names
  (Container, TextBlock, frappe-ui components, custom components)
- `template-walker.js` — converts Vue template AST to Studio block
  trees; handles containers, text/interpolation nodes, v-if, v-for,
  Teleport, named slots; extracts Tailwind classes and inline styles;
  rewrites component-prop references to `inputs.propName` so Studio's
  evaluation context resolves them correctly
- `script-analyzer.js` — extracts `createResource` calls, `ref`
  variables, `watch` watchers and `defineProps`/`defineModel` props
  from `<script setup>` blocks

### Python importer (`studio/importer.py`)
- Invokes the Node parser via subprocess, receives a JSON manifest
- Upserts Studio App, Studio Pages and Studio Components
- Builds correct variable rows (JSON-encodes String type values so
  `codeStore.evaluateDynamicValues` can parse them)
- `Studio Import Log` doctype tracks status, counts and errors

### API & UI
- `studio/api.py` — three whitelisted endpoints:
  `get_importable_apps`, `preview_import`, `start_import`
- `ImportPanel.vue` — left-panel tab that lists importable apps,
  shows a page/component preview with checkboxes, and runs the import
- `StudioLeftPanel.vue` — adds "Import" tab to the sidebar

## Rendering fixes discovered during testing (CRM)

| Issue | Fix |
|---|---|
| `componentName` case mismatch (PascalCase vs kebab-case) | `toComponentId()` in classifier maps custom component names to `component_id` |
| `JSON.parse` crash on empty variable initial values | `json.dumps()` for String type; `"null"` for empty Object type |
| Empty canvas — `canHaveChildren()` always false | `buildRootBlock` sets `componentId:"root"` + `originalElement:"div"` |
| Empty custom components (Teleport/slot) | Teleport → transparent; `<slot>` → render fallback children; `isComponentContext` flag suppresses v-if conditions that reference component-local state |
| Missing text content | `walkNode` handles `NodeTypes.TEXT` and `INTERPOLATION`; `extractTextContent` collects COMPOUND_EXPRESSION parts |
| Dynamic classes / inline styles | `extractClasses` captures `:class="[...]"` quoted literals; `extractInlineStyles` parses CSS strings and `:style` objects into camelCase property maps |
| Component-prop expressions not resolving | Template walker rewrites `{{ routeName }}` → `{{ inputs.routeName }}` when parsing component templates, matching Studio's `{ inputs: {...} }` evaluation context; `defineModel` variables are mapped to their underlying prop (`modelValue`) |

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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