Skip to content

fix(create-vc-app): make scaffolded apps build and lint out of the box - #302

Open
maksimzinchuk wants to merge 2 commits into
mainfrom
fix/scaffolding-lint-and-format
Open

fix(create-vc-app): make scaffolded apps build and lint out of the box#302
maksimzinchuk wants to merge 2 commits into
mainfrom
fix/scaffolding-lint-and-format

Conversation

@maksimzinchuk

@maksimzinchuk maksimzinchuk commented Aug 17, 2026

Copy link
Copy Markdown
Collaborator

Problem

Three defects reported against the scaffolding, all confirmed:

  1. A generated app did not build. Welcome.vue used tw-text-s, which is not in the framework's fontSize scale (xxs / xs / sm / base / lg / …). @apply with a non-existent class is a hard PostCSS error, not a silent no-op:

    [vite:css] [postcss] Welcome.vue?vue&type=style&index=0&lang.scss:20:3:
      The `tw-text-s` class does not exist.
    
  2. yarn lint failed out of the box. eslint-plugin-import and eslint-import-resolver-typescript were declared in the template's devDependencies but never wired into eslint.config.mjs, while Welcome.vue and routes.ts.ejs carried eslint-disable comments for import/no-unresolved. ESLint treats a disable directive for an unregistered rule as an error:

    1:1  error  Definition for rule 'import/no-unresolved' was not found
    ✖ 1 problem (1 error)
    
  3. Template code was never linted or formatted. cli/create-vc-app/src/templates was listed in both .prettierignore and the root eslint ignores, so even the plain .ts/.vue files there were unchecked. prettier --check flagged 8 files, including a tsconfig.json with broken indentation.

The root cause behind all three: templates are .ejs, so ESLint and Prettier structurally cannot check them. Nothing verified this code, and the extra ignore entries hid even the files that were checkable.

Approach

Templates cannot be linted directly, so the gate has to be generate a project and check the output. That needs two independent layers — neither alone is sufficient:

  • EJS whitespace slurping (<%_ … -%>). A bare <% if %> on its own line emits a blank line into the output. bootstrap.ts.ejs and main.ts.ejs already did this correctly; routes.ts.ejs did not, producing output riddled with stray blank lines.
  • A Prettier pass over the generated tree (src/engine/format.ts), called from init (whole tree) and add-module (only the touched paths, so it never rewrites files the user owns). This is what normalises the regex splicing in codegen.ts.

Prettier alone cannot replace layer 1: it collapses 2+ blank lines to one but preserves a single blank line between statements, so EJS artifacts survive a format pass. Layer 1 alone cannot replace layer 2 either, since it does nothing about the string splicing in codegen.ts.

Changes

  • Fix tw-text-stw-text-sm; drop the unused bgImage import.
  • Wire eslint-plugin-import + the typescript resolver in the app template and in the monorepo root. flatConfigs.recommended must be spread before the Vue/TS configs (it sets a global languageOptions.ecmaVersion with no files scope and would otherwise override their parsers), and the resolver setting must come after flatConfigs.typescript (which presets a node resolver).
  • Disable import/namespace, import/default, import/no-named-as-default{,-member}. These parse the dependency's source with espree and produce 48 bogus Parse errors in imported module '@vc-shell/framework' on a real app. TypeScript already covers them; flatConfigs.typescript only disables import/named.
  • Narrow the .prettierignore and root eslint ignores entries to **/_yarn, so the plain template files are covered by yarn format and yarn lint again; format the 8 files this surfaced.
  • Add 8 tests. Four assert that a generated project is Prettier-clean across flag combinations; four assert the raw renderDir output is free of EJS whitespace artifacts. The second group is required — without it the formatter would silently absorb template bugs.
  • Collapse the duplicate imports that the newly enabled import/no-duplicates reports (10 framework files, plus three separate from "vue" statements in PullToRefresh.vue that --fix could not merge).

framework/index.ts exports ~18 names both explicitly and via export * from the same module. That is legal — an explicit export shadows a star export — but import/export cannot tell it apart from a real collision, so the rule is disabled for that one file. Cleaning up the barrel is left as a follow-up.

Second commit: add-module codegen

Writing the tests above surfaced that codegen.ts — which edits existing source with regexes — had no test coverage and three defects. Each was reproduced before being fixed.

add-module never registered the module. The anchor was /\.use\([A-Z]\w+,\s*\{\s*router\s*\}\)/, but the template emits app.use(Orders);. Nothing matched, lastMatch stayed null, no app.use() was inserted — and the command still printed:

✔ Updated src/main.ts — added import & app.use(Reviews)

The module was imported, unused, and never registered, so the feature silently did nothing. This is current behaviour on main, not a hypothetical.

A multi-line trailing import got corrupted. insertImport matched /^import\s.+$/gm, which sees only the first line of a multi-line import:

import {
import Orders from "./modules/orders";   // ← spliced inside the braces
  notification,
} from "@vc-shell/framework";

A nested }); truncated the menu-item match. /addMenuItem\(\{[\s\S]*?\}\);/ is lazy, so any menu item containing a callback that ends in }); caused the new item to land inside the previous one's body.

Structural edits now run against a mask of the source with comment and string contents replaced by spaces and offsets preserved, so a brace or semicolon inside a string cannot be read as syntax. Call boundaries come from bracket matching instead of a lazy quantifier, nested calls are skipped, imports are scanned to their terminating ;, and insertions apply highest-offset-first so earlier positions stay valid. addModuleToMain throws when an anchor is missing rather than reporting a change it did not make — add-module already catches that and prints manual instructions, and the file is left untouched.

codegen.test.ts adds 9 cases. Each result is run through Prettier, which throws on a syntax error; that check alone would have caught two of the three defects.

magicast is declared in cli/create-vc-app/dependencies and never imported anywhere — a dead dependency, and ironically a library built for exactly this kind of edit. Left alone here; removing it or migrating codegen.ts onto it is a separate call.

Verification

Scaffolded an app with the built CLI (standalone, module, dashboard, tenant routes, mocks) plus add-module, then ran it for real:

Step Result
yarn install exit 0
yarn build (vite build + vue-tsc) exit 0, dist/index.html + dist/types/ produced
yarn type-check exit 0
yarn lint exit 0, 0 errors
yarn preview → HTTP 200, correct title, CSS and JS bundles served
Headless boot Vue mounts, redirects to #/login, login form renders, no pageerror

After add-module reviews, main.ts now reads:

  app.use(Orders);
  app.use(Sample);
  app.use(Reviews);   // ← previously missing entirely
  app.use(router);

and the app with the registered module still builds, type-checks and lints clean.

The emitted CSS is correct, not merely non-crashing:

.dashboard-widget-welcome__welcome-description{…;font-size:.875rem;…}

.875rem is exactly the sm value from framework/tailwind.config.ts.

Negative controls — each fix was checked to be load-bearing:

  • Restoring tw-text-s in the built app fails vite build with the PostCSS error above.
  • Reverting the EJS slurping fails the 4 template tests while the 4 Prettier tests stay green, confirming the two layers are independent.
  • Disabling formatGenerated fails only the 4 Prettier tests.

All CI gates were run in a fresh, unbuilt worktree (matching what CI sees): lint:check, format:check, stylelint:check, typecheck, check:locales, check:circular, check:layers — all exit 0. Framework suite: 3791 tests in 403 files pass. CLI suite: 67 tests (was 50).

Three things made a local green run misleading, which is worth knowing for anyone touching these configs:

  • CI lints without building (yarn install --immutableyarn lint:check, no build step in between). Every @vc-shell/* workspace package resolves through main/types fields pointing into dist/, so none of them resolve on CI. A leftover framework/dist hides this locally.
  • Resolver fallback to src/ is platform-dependent. With the ignore narrowed to ^@vc-shell/framework/, the Linux runner reported 6 import/no-unresolved errors for @vc-shell/mf-config and @vc-shell/config-generator that macOS did not — reproduced neither by deleting framework/dist nor by a clean yarn install --immutable. eslint-import-resolver-typescript v4 uses the native unrs-resolver, which behaves differently per platform. Hence the ignore covers ^@vc-shell/ rather than relying on that fallback.
  • yarn lintyarn lint:check. The former runs --fix and ignores warnings; CI runs the latter with --max-warnings=0. A green yarn lint says nothing about CI.

Notes

A freshly generated app still reports 8 @typescript-eslint/no-unused-vars warnings (yarn lint exits 0). These are intentional stubs: the parameters sit next to commented-out TODO API calls that reference them, so renaming to _query would break the code the user is meant to uncomment. Left as-is deliberately.

@github-actions

github-actions Bot commented Aug 17, 2026

Copy link
Copy Markdown

📦 Preview published for commit 45ba88b

Install the preview with dist-tag:

npm install @vc-shell/framework@pr-302

Or pin to the exact commit:

npm install @vc-shell/framework@2.4.0-pr302.45ba88b

Published packages (dist-tag pr-302, version 2.4.0-pr302.45ba88b):

  • @vc-shell/framework
  • @vc-shell/api-client-generator
  • @vc-shell/create-vc-app
  • @vc-shell/config-generator
  • @vc-shell/migrate
  • @vc-shell/ts-config
  • @vc-shell/mf-config
  • @vc-shell/mf-host
  • @vc-shell/mf-module
  • @vc-shell/vc-app-skill

`tw-text-s` is not in the framework's fontSize scale (xxs/xs/sm/base/...), so
`@apply` failed at the PostCSS stage and `yarn build` errored in every generated
app. `Welcome.vue` and `routes.ts.ejs` also carried `eslint-disable` comments for
`import/no-unresolved` — a rule no config registered — which made `yarn lint`
fail with "Definition for rule 'import/no-unresolved' was not found".

Templates are `.ejs`, so ESLint and Prettier structurally cannot check them, and
the templates directory was additionally listed in `.prettierignore` and the root
eslint `ignores`. Nothing verified this code. Both ignores are now narrowed to
`**/_yarn`, and the generated output is gated by tests instead.

- fix the class name and drop the unused `bgImage` import
- wire eslint-plugin-import and the typescript resolver in the app template and
  in the monorepo root; disable the rules that parse dependency sources with
  espree, which chokes on modern syntax in `dist` output
- slurp EJS whitespace in `routes.ts.ejs`: bare `<% %>` control tags emitted
  blank lines that a Prettier pass cannot remove, since Prettier preserves a
  single blank line between statements
- run Prettier over the generated tree from `init`, and over just the touched
  paths from `add-module`, so the regex splicing in `codegen.ts` is normalised.
  `resolveConfig` is called with `editorconfig: true`, otherwise the API ignores
  `.editorconfig` and rewraps at 80 columns instead of the project's 120
- add 8 tests: generated projects must be Prettier-clean across four flag
  combinations, and the raw `renderDir` output must be free of EJS whitespace
  artifacts. The second layer is required because the formatter would otherwise
  mask template bugs
- collapse duplicate imports the newly enabled `import/no-duplicates` reports

Scaffolding a project and running `yarn install && yarn build && yarn type-check
&& yarn lint` now passes, and the app boots to the login page. Restoring
`tw-text-s` fails the build again.
@maksimzinchuk
maksimzinchuk force-pushed the fix/scaffolding-lint-and-format branch from e9fab59 to 99e48e1 Compare August 17, 2026 14:09
…ing edits

`codegen.ts` edited existing source with regexes and had no test coverage. Three
defects, each reproduced before fixing:

- `add-module` never registered the module. The anchor pattern was
  `/\.use\([A-Z]\w+,\s*\{\s*router\s*\}\)/`, but the template emits
  `app.use(Orders);`. Nothing matched, `lastMatch` stayed null, no `app.use()`
  was inserted — and the command still printed
  "✔ Updated src/main.ts — added import & app.use(Reviews)". The module was
  imported, unused, and never registered, so it simply did not work.
- `insertImport` matched `/^import\s.+$/gm`, which sees only the first line of a
  multi-line import. When the last import spanned lines, the new statement was
  spliced between `import {` and its first specifier.
- `addMenuItemToBootstrap` matched `/addMenuItem\(\{[\s\S]*?\}\);/` lazily, so a
  menu item containing a literal `});` (any nested callback) truncated the match
  and the new item landed inside the previous one's body.

Structural edits now run against a mask of the source in which comment and
string contents are replaced by spaces, offsets preserved. Call boundaries come
from bracket matching rather than a lazy quantifier, nested calls are skipped,
import statements are scanned to their terminating `;`, and insertions are
applied highest-offset-first so earlier positions stay valid.

`addModuleToMain` now throws when an anchor is missing instead of reporting a
change it did not make; `add-module` already catches that and prints what to add
by hand, and the file is left untouched.

Adds `codegen.test.ts` (9 cases, the file had none). Each result is run through
Prettier, which throws on a syntax error — that alone would have caught two of
the three defects.
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