Skip to content

refactor(rootage): replace per-feature config keys with a plugin system - #1876

Open
junghyeonsu wants to merge 3 commits into
devfrom
feat/rootage-config
Open

refactor(rootage): replace per-feature config keys with a plugin system#1876
junghyeonsu wants to merge 3 commits into
devfrom
feat/rootage-config

Conversation

@junghyeonsu

@junghyeonsu junghyeonsu commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

배경 — config가 기능마다 키를 늘리는 구조였습니다

rootage CLI를 부르는 패키지는 7개인데 옵션이 전부 스크립트 플래그로 반복됐습니다(--prefix seed 7곳, --generator 2곳). 이걸 config로 옮기는 첫 설계는 기능마다 일회성 키를 갖는 모양이었는데(tokenCss.generator, componentSpec.dtsBanner…), 새 요구가 생길 때마다 스키마가 자라고 CLI가 그 모양을 전부 알아야 했습니다.

그래서 config를 { prefix, plugins }로 닫았습니다. 새 요구는 스키마 변경 없이 플러그인으로 들어옵니다. #1795 리뷰에서 @te6-in 님이 지적한 "rootage core/cli 밖으로" 방향의 종착점입니다.

플러그인 시스템

// @seed-design/rootage-core/config
interface RootagePlugin {
  name: string;
  transform?: (file: GeneratedFile, ctx: PluginContext) => string | undefined | Promise<string | undefined>;
  tokenCssGenerator?: GeneratorFunction; // 기존 --generator 흡수
}

transform은 모든 커맨드가 지나는 쓰기 직전 단일 choke point에서 돌아서, 생성물 전부(dts/mjs/css/json/ts)에 자동으로 적용됩니다. 파일 메타의 kind는 rootage 스펙의 판별자를 그대로 재사용합니다 — Authoring.ModelKind(= Model["kind"])로 export해서 소비자도 씁니다.

설계는 기존 플러그인 시스템들의 합의를 따랐습니다:

규약 출처
name 필수, 훅 실패는 [이름] transform failed로 귀속 rollup name · postcss postcssPlugin · stackflow key
옵션 검증은 플러그인 팩토리 소유, CLI는 모양만 검증 ESLint rule schema
undefined 반환 = 통과, 배열 순서 = 실행 순서(체인) rollup transform
독점 훅(tokenCssGenerator)은 first-wins + 복수 제공 시 경고 rollup "first" hook kind
플러그인은 문자열이 아니라 import한 값 ESLint flat config 전환

훅은 지금 쓰는 둘만 정의했습니다. 플러그인이 객체라 훅 추가는 non-breaking이므로, buildStart/End 같은 라이프사이클 훅은 쓰는 곳이 생길 때 붙이면 됩니다. qvism은 이번에 건드리지 않습니다postcssPlugins 패스스루가 이미 주 확장 지점을 커버하고 있어, 같은 인터페이스가 검증된 뒤 후속 검토합니다.

내장 jsdoc 플러그인

@seed-design/rootage-core/plugins/jsdoc — 생성되는 .d.ts에 doc comment를 붙입니다. JSDoc 조립 지식은 엔진이 소유합니다: 본문 → 빈 줄 → @태그 순서로 렌더링하는데, 태그가 앞에 오면 TS가 뒤따르는 줄을 전부 태그의 부속 텍스트로 파싱해 hover documentation이 비어버립니다(언어 서비스로 실측). 소비자는 산문과 태그 이름만 주므로 이 함정을 알 필요도, 틀릴 방법도 없습니다.

  • target은 스펙 kind 어휘("ComponentSpec") — 오타는 에디터에서 union 에러
  • tag단수(release 태그는 상호 배타 — api-extractor도 항목당 하나 강제) open string(JSDoc 태그는 설계상 open-ended — 새 태그에 엔진 패치를 강제하지 않음)
  • SDK 미채용 근거: @microsoft/tsdoc은 런타임 클래스뿐 태그명 union·modifier 구분 타입 미제공, comment-parser는 저수준 토큰 시드 필요, ts.factory.createJSDocComment는 typescript 런타임 의존성 — 공통으로 config의 @type 경유 소비자 에디터까지 의존성이 전파됩니다. 렌더러는 6줄입니다.

config 타입

/** @type {import("@seed-design/rootage-core/config").RootageConfig} */
export default { prefix: "seed" };

실측: prefixx 오타 → Did you mean to write 'prefix'?, target: "component-spec"(소문자) → union 에러. 강제는 에디터 전용이고 실차단은 CLI 런타임 검증이 합니다(어긋난 키를 짚는 hand-written 에러 — core 컨벤션상 zod 미사용).

변경 사항

  • core: src/config.ts(타입) + src/plugins/jsdoc.ts(내장 플러그인) 신설, ./config·./plugins/jsdoc 서브패스(./generator 격리와 동일 레시피), Authoring.ModelKind export
  • cli: cosmiconfig 로딩(qvism 미러, 우선순위 플래그 > config), 플러그인 러너, --generator 플래그 제거
  • 소비자 7개: config 6개 신설(qvism preset 2개는 seed-css generator를 플러그인으로), 스크립트에서 플래그 제거. rootage-artifacts는 설정할 옵션이 없어 config 없음
  • 테스트: rootage CLI는 테스트가 0개였습니다 — qvism의 Bun.spawn + temp dir 패턴으로 6케이스(우선순위·플러그인 적용·exclude·에러 귀속), jsdoc 플러그인 단위 7케이스

검증

  • bun rootage:test124 pass / 0 fail
  • 7개 소비자 전부 재생성 → 실질 diff 0 (배럴 index·token.css·tailwind4 index.css·tokens.ts의 순서 뒤바뀜만 관찰 — 내용 집합 동일. 플래그 방식으로도 재현되는 기존 비결정성이라 커밋 제외. 원인은 readYAMLFilesSync의 파일시스템 순서 의존으로 보이며 별건감)
  • changeset 없음 (core/cli는 private, 소비자 변경은 스크립트·미배포 config뿐)

🤖 Generated with Claude Code

Summary by CodeRabbit

  • 새로운 기능

    • Rootage CLI가 설정 파일을 자동으로 탐색하고 지정된 경로에서 불러옵니다.
    • 접두사와 플러그인 설정을 지원하며, 잘못된 설정에는 파일 경로와 오류 원인을 안내합니다.
    • 생성 결과에 플러그인 변환을 적용하고, 실패 시 관련 플러그인과 파일 정보를 제공합니다.
    • JSDoc 주석을 생성하는 플러그인이 추가되었으며 대상 및 포함·제외 조건을 설정할 수 있습니다.
    • 여러 프리셋에 기본 seed 접두사와 토큰 CSS 생성 설정이 적용되었습니다.
  • 버그 수정

    • 설정이 없거나 비어 있는 환경에서도 CLI가 정상적으로 동작합니다.

Consumers declared their options as CLI flags repeated across seven
package.json scripts — `--prefix seed` in all of them, `--generator` in
two. There was also no way to declare anything the engine had to keep
per-consumer state about, so such policy had nowhere to live but inside
rootage itself.

Mirror the qvism config layer: cosmiconfig looks for rootage.config.mjs
in the working directory, and flags still win over it. The CLI resolves
`prefix`, `tokenCss.generator`, and `componentSpec.dtsBanner`, the last
of which is declarative — `{ content, ignore }` — so a consumer can state
which generated specs opt out without the engine hardcoding an id.

Config is validated by hand with a message naming the offending key,
following the core convention of no zod. Generated output is unchanged
across all seven consumers.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@changeset-bot

changeset-bot Bot commented Aug 2, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: 6917b22

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@coderabbitai

coderabbitai Bot commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Rootage core에 플러그인 설정 타입(RootageConfig, RootagePlugin, GeneratedFile, PluginContext)과 ModelKind 타입, jsdoc 플러그인을 추가했다. CLI는 cosmiconfig 기반 config 로더를 도입하고, 생성 명령이 플러그인 transform 훅과 tokenCssGenerator를 적용하도록 변경했다. 여러 패키지에 rootage.config.mjs를 추가했다.

Changes

Rootage 플러그인 시스템

Layer / File(s) Summary
core 플러그인 타입 정의
ecosystem/rootage/core/src/config.ts, ecosystem/rootage/core/src/parser/authoring/types.ts
GeneratedFile, PluginContext, RootagePlugin, RootageConfig 인터페이스와 ModelKind 타입을 추가했다.
jsdoc 플러그인 구현
ecosystem/rootage/core/src/plugins/jsdoc.ts, ecosystem/rootage/core/src/plugins/jsdoc.test.ts
ComponentSpec의 .d.ts 파일에 조건부로 JSDoc 주석을 삽입하는 플러그인과 옵션 검증 로직을 추가했다. 테스트로 본문/태그 조립, include/exclude, 오류 처리를 확인했다.
CLI config 로더
ecosystem/rootage/cli/src/config.ts
cosmiconfig로 설정 파일을 검색·로드하고, prefix와 plugins 필드 타입을 검증하는 loadConfig 함수를 추가했다.
CLI 파일 기록 경로 플러그인 적용
ecosystem/rootage/cli/src/index.ts
활성 플러그인과 컨텍스트를 관리하고, transform 훅을 순차 적용하는 비동기 기록 함수를 도입해 토큰, 컴포넌트 스펙, tailwind, 교환 산출물 기록을 전환했다.
CLI 명령별 config 초기화
ecosystem/rootage/cli/src/index.ts
전역 config 옵션을 추가하고, 각 생성 명령이 config를 로드해 플러그인과 prefix를 초기화하며 명령행 prefix를 우선 적용하도록 변경했다.
CLI 통합 테스트
ecosystem/rootage/cli/src/index.test.ts
config prefix 반영, prefix 우선순위, jsdoc 대상 처리, config 부재, 잘못된 설정 오류, transform 예외 처리를 검증했다.
패키지별 rootage.config.mjs
packages/lynx-qvism-preset/rootage.config.mjs, packages/qvism-preset/rootage.config.mjs, packages/tailwind3-plugin/rootage.config.mjs, packages/tailwind4-theme/rootage.config.mjs
seed prefix와 tokenCssGenerator를 설정한 config 파일을 각 패키지에 추가했다.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant CLI as index.ts
  participant Config as config.ts (loadConfig)
  participant Plugin as RootagePlugin (jsdoc)
  participant FS as writeGenerated

  CLI->>Config: loadConfig(configPath)
  Config-->>CLI: RootageConfig (prefix, plugins)
  CLI->>Plugin: transform(GeneratedFile, PluginContext)
  Plugin-->>CLI: 변환된 code
  CLI->>FS: writeGenerated(파일 메타데이터)
  FS-->>CLI: 기록 완료
Loading

Possibly related PRs

  • daangn/seed-design#1759: 두 PR 모두 ecosystem/rootage/cli/src/index.ts의 component-spec 생성과 플러그인/컨텍스트 기반 처리를 수정한다.
  • daangn/seed-design#1885: 두 PR 모두 ecosystem/rootage/cli/src/index.ts에서 JSON 교환 산출물과 메타데이터를 포함한 Rootage 생성 기능을 확장한다.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed 제목은 기능별 설정 키를 플러그인 시스템으로 대체하는 변경의 핵심 내용을 정확하고 간결하게 설명합니다.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/rootage-config

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@junghyeonsu junghyeonsu changed the title feat/rootage config feat(rootage): load per-package options from rootage.config.mjs Aug 2, 2026
@github-actions

github-actions Bot commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

Alpha Preview (Stackflow SPA)

@github-actions

github-actions Bot commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

Alpha Preview (Storybook)

@github-actions

github-actions Bot commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

Alpha Preview (Docs)

junghyeonsu and others added 2 commits August 4, 2026 22:58
# Conflicts:
#	ecosystem/rootage/core/src/languages/typescript.test.ts
The config was growing a key per feature — tokenCss.generator for the
custom emitter, componentSpec.dtsBanner for the doc comment — each one a
schema change and another shape the CLI had to know. Close the config at
{ prefix, plugins } instead: new needs arrive as plugins, not keys.

A plugin is { name, transform?, tokenCssGenerator? }, following the
conventions the established systems agree on: a required name so hook
failures are attributed ("[name] transform failed"), transforms chained
in array order with undefined passing through (Rollup), options validated
by the plugin's own factory rather than the host (ESLint), and plugins as
imported values rather than resolvable strings (ESLint flat config).
transform runs at the single write choke point, so it covers every
generated file with { path, code, type, kind, id } metadata; kind reuses
the spec's own discriminator, exported as Authoring.ModelKind.

jsdoc ships as a built-in plugin at rootage-core/plugins/jsdoc. It owns
the comment assembly — text first, then a blank line, then the tag —
because a leading tag makes TypeScript parse every following line as the
tag's own text, leaving the symbol documentation empty in hover. Authors
supply prose and a tag name; the ordering cannot be gotten wrong.

The --generator flag folds into the tokenCssGenerator hook, and the qvism
presets now pass their seed-css generator through config. Consumer configs
gain a @type annotation against rootage-core/config; misspelled keys and
targets fail right in the editor, and the CLI still validates shapes at
load time. Generated output is unchanged across all seven consumers.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@junghyeonsu junghyeonsu changed the title feat(rootage): load per-package options from rootage.config.mjs refactor(rootage): replace per-feature config keys with a plugin system Aug 4, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@ecosystem/rootage/cli/src/config.ts`:
- Around line 13-25: Add validation to reject unknown configuration keys in both
the plugin object validation (where name, transform, and tokenCssGenerator are
currently validated) and the RootageConfig validation section (lines 28-47).
After validating the known fields in each object, check that no additional
unexpected keys exist beyond the allowed set and call fail() with the filepath
included in the error message if any unknown keys are found. This will catch
common typos like `prefx` or `tranform` and alert users that their configuration
is not being applied.

In `@ecosystem/rootage/cli/src/index.ts`:
- Around line 175-189: Remove the kind metadata from both writeGenerated calls
that create the aggregate index.mjs and index.d.ts files in the surrounding
generation flow. Keep kind on individual generated ComponentSpec files, and add
a test confirming index.d.ts is not processed by the ComponentSpec-targeted
transform.
- Around line 462-466: Update the json-ts command’s pluginContext construction
in the argv handler to use argv.prefix when provided, falling back to
config.prefix, matching the precedence used by other commands such as the flow
around line 381. Add or update a test that verifies transform plugins receive
the CLI prefix override.

In `@ecosystem/rootage/core/src/plugins/jsdoc.ts`:
- Around line 36-45: [jsdoc] 팩토리의 옵션 런타임 검증을 확장하세요. `target`은 `"ComponentSpec"`만
허용하고, `tag`가 지정되면 비어 있지 않은 문자열이어야 하며, `include`와 `exclude`는 각각 문자열 배열인지 검증해 잘못된
값에서 즉시 오류를 발생시키세요. 해당 오류 경로를 테스트로 추가하고 유효한 옵션의 기존 동작은 유지하세요.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 11b58d8a-e4e9-4544-8f3e-119ae06560c3

📥 Commits

Reviewing files that changed from the base of the PR and between 8e6bd1b and 6917b22.

⛔ Files ignored due to path filters (11)
  • bun.lock is excluded by !**/*.lock
  • ecosystem/rootage/cli/package.json is excluded by !**/package.json
  • ecosystem/rootage/core/package.json is excluded by !**/package.json
  • packages/css/package.json is excluded by !**/package.json, !packages/css/**/*
  • packages/css/rootage.config.mjs is excluded by !packages/css/**/*
  • packages/lynx-css/package.json is excluded by !**/package.json, !packages/lynx-css/**/*
  • packages/lynx-css/rootage.config.mjs is excluded by !packages/lynx-css/**/*
  • packages/lynx-qvism-preset/package.json is excluded by !**/package.json
  • packages/qvism-preset/package.json is excluded by !**/package.json
  • packages/tailwind3-plugin/package.json is excluded by !**/package.json
  • packages/tailwind4-theme/package.json is excluded by !**/package.json
📒 Files selected for processing (11)
  • ecosystem/rootage/cli/src/config.ts
  • ecosystem/rootage/cli/src/index.test.ts
  • ecosystem/rootage/cli/src/index.ts
  • ecosystem/rootage/core/src/config.ts
  • ecosystem/rootage/core/src/parser/authoring/types.ts
  • ecosystem/rootage/core/src/plugins/jsdoc.test.ts
  • ecosystem/rootage/core/src/plugins/jsdoc.ts
  • packages/lynx-qvism-preset/rootage.config.mjs
  • packages/qvism-preset/rootage.config.mjs
  • packages/tailwind3-plugin/rootage.config.mjs
  • packages/tailwind4-theme/rootage.config.mjs

Comment on lines +13 to +25
const { name, transform, tokenCssGenerator } = value as Record<string, unknown>;

if (typeof name !== "string" || name === "") {
fail(filepath, `plugins[${index}].name must be a non-empty string`);
}
if (transform !== undefined && typeof transform !== "function") {
fail(filepath, `plugins[${index}] (${name}): transform must be a function`);
}
if (tokenCssGenerator !== undefined && typeof tokenCssGenerator !== "function") {
fail(filepath, `plugins[${index}] (${name}): tokenCssGenerator must be a function`);
}

return value as RootagePlugin;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

허용되지 않은 설정 키를 오류로 처리하십시오.

Line 13Line 33은 알려진 필드만 검사합니다. prefx는 반환된 config에서 사라지고, tranform은 어떤 hook에도 연결되지 않지만 오류를 만들지 않습니다. 사용자는 설정이 적용되지 않은 상태로 기본 생성 결과를 받습니다.

RootageConfig와 각 plugin 객체에 허용 키 목록을 적용하십시오. prefxtranform이 filepath 포함 오류를 내는 통합 테스트도 추가하십시오.

수정 예시
 function validatePlugin(value: unknown, index: number, filepath: string): RootagePlugin {
   if (typeof value !== "object" || value === null) {
     fail(filepath, `plugins[${index}] must be an object`);
   }
 
+  const unknownKey = Object.keys(value).find(
+    (key) => !["name", "transform", "tokenCssGenerator"].includes(key),
+  );
+  if (unknownKey) {
+    fail(filepath, `plugins[${index}].${unknownKey} is not supported`);
+  }
+
   const { name, transform, tokenCssGenerator } = value as Record<string, unknown>;
@@
 function validateConfig(value: unknown, filepath: string): RootageConfig {
   if (typeof value !== "object" || value === null) {
     fail(filepath, "config must export an object");
   }
 
+  const unknownKey = Object.keys(value).find(
+    (key) => !["prefix", "plugins"].includes(key),
+  );
+  if (unknownKey) {
+    fail(filepath, `config.${unknownKey} is not supported`);
+  }
+
   const { prefix, plugins } = value as Record<string, unknown>;

Also applies to: 28-47

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ecosystem/rootage/cli/src/config.ts` around lines 13 - 25, Add validation to
reject unknown configuration keys in both the plugin object validation (where
name, transform, and tokenCssGenerator are currently validated) and the
RootageConfig validation section (lines 28-47). After validating the known
fields in each object, check that no additional unexpected keys exist beyond the
allowed set and call fail() with the filepath included in the error message if
any unknown keys are found. This will catch common typos like `prefx` or
`tranform` and alert users that their configuration is not being applied.

Comment on lines +175 to 189
await writeGenerated({
path: "index.mjs",
code: tsStringifier.getComponentSpecIndexMjs(specs),
type: "mjs",
kind: "ComponentSpec",
writePath: path.join(process.cwd(), dir, "index.mjs"),
});

const dtsIndexCode = tsStringifier.getComponentSpecIndexDts(specs);
const dtsIndexWritePath = path.join(process.cwd(), dir, "index.d.ts");

writeFileSync({
filename: "index",
code: dtsIndexCode,
writePath: dtsIndexWritePath,
await writeGenerated({
path: "index.d.ts",
code: tsStringifier.getComponentSpecIndexDts(specs),
type: "dts",
kind: "ComponentSpec",
writePath: path.join(process.cwd(), dir, "index.d.ts"),
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

집계 파일에서 kind 메타데이터를 제거하십시오.

Line 179Line 187index 집계 파일을 "ComponentSpec" 원본 파일처럼 표시합니다. GeneratedFile.kind는 단일 스펙의 종류만 표현하며 index 같은 집계 파일에는 값을 두지 않습니다. 따라서 kind를 기준으로 필터링하는 plugin이 index.d.ts를 개별 ComponentSpec 산출물로 잘못 변환할 수 있습니다.

kind 필드를 제거하십시오. index.d.ts가 ComponentSpec 대상 transform을 받지 않는 테스트를 추가하십시오.

수정 예시
   await writeGenerated({
     path: "index.mjs",
     code: tsStringifier.getComponentSpecIndexMjs(specs),
     type: "mjs",
-    kind: "ComponentSpec",
     writePath: path.join(process.cwd(), dir, "index.mjs"),
   });
@@
   await writeGenerated({
     path: "index.d.ts",
     code: tsStringifier.getComponentSpecIndexDts(specs),
     type: "dts",
-    kind: "ComponentSpec",
     writePath: path.join(process.cwd(), dir, "index.d.ts"),
   });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
await writeGenerated({
path: "index.mjs",
code: tsStringifier.getComponentSpecIndexMjs(specs),
type: "mjs",
kind: "ComponentSpec",
writePath: path.join(process.cwd(), dir, "index.mjs"),
});
const dtsIndexCode = tsStringifier.getComponentSpecIndexDts(specs);
const dtsIndexWritePath = path.join(process.cwd(), dir, "index.d.ts");
writeFileSync({
filename: "index",
code: dtsIndexCode,
writePath: dtsIndexWritePath,
await writeGenerated({
path: "index.d.ts",
code: tsStringifier.getComponentSpecIndexDts(specs),
type: "dts",
kind: "ComponentSpec",
writePath: path.join(process.cwd(), dir, "index.d.ts"),
});
await writeGenerated({
path: "index.mjs",
code: tsStringifier.getComponentSpecIndexMjs(specs),
type: "mjs",
writePath: path.join(process.cwd(), dir, "index.mjs"),
});
await writeGenerated({
path: "index.d.ts",
code: tsStringifier.getComponentSpecIndexDts(specs),
type: "dts",
writePath: path.join(process.cwd(), dir, "index.d.ts"),
});
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ecosystem/rootage/cli/src/index.ts` around lines 175 - 189, Remove the kind
metadata from both writeGenerated calls that create the aggregate index.mjs and
index.d.ts files in the surrounding generation flow. Keep kind on individual
generated ComponentSpec files, and add a test confirming index.d.ts is not
processed by the ComponentSpec-targeted transform.

Comment on lines +462 to +466
async (argv) => {
console.log("Start");
const config = await loadConfig(argv.config);
activePlugins = config.plugins ?? [];
pluginContext = { prefix: config.prefix };

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

json-ts에도 유효한 prefix를 전달하십시오.

Line 466config.prefixPluginContext에 전달합니다. Line 381 등의 다른 명령은 argv.prefix ?? config.prefix를 사용합니다. json-ts --prefix flag를 실행하면 transform plugin은 flag 대신 config 값을 받습니다.

CLI 플래그 우선순위를 동일하게 적용하고, transform이 받은 PluginContext.prefix를 검증하는 테스트를 추가하십시오.

수정 예시
-      pluginContext = { prefix: config.prefix };
+      pluginContext = { prefix: argv.prefix ?? config.prefix };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
async (argv) => {
console.log("Start");
const config = await loadConfig(argv.config);
activePlugins = config.plugins ?? [];
pluginContext = { prefix: config.prefix };
async (argv) => {
console.log("Start");
const config = await loadConfig(argv.config);
activePlugins = config.plugins ?? [];
pluginContext = { prefix: argv.prefix ?? config.prefix };
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ecosystem/rootage/cli/src/index.ts` around lines 462 - 466, Update the
json-ts command’s pluginContext construction in the argv handler to use
argv.prefix when provided, falling back to config.prefix, matching the
precedence used by other commands such as the flow around line 381. Add or
update a test that verifies transform plugins receive the CLI prefix override.

Comment on lines +36 to +45
export function jsdoc(options: JsdocPluginOptions): RootagePlugin {
if (typeof options?.text !== "string" || options.text === "") {
throw new Error("jsdoc plugin: `text` must be a non-empty string");
}
if (
options.tag !== undefined &&
(typeof options.tag !== "string" || options.tag.startsWith("@"))
) {
throw new Error('jsdoc plugin: `tag` must be a tag name without "@"');
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

플러그인 옵션을 런타임에서 완전히 검증하세요.

rootage.config.mjs의 값은 TypeScript 타입 검사를 거치지 않습니다. 잘못된 target은 변환을 조용히 생략합니다. 배열이 아닌 include 또는 excludematches에서 .includes 호출을 실패시킬 수 있습니다. 빈 tag도 허용되지만 출력에서는 생략됩니다.

팩토리에서 target === "ComponentSpec", 비어 있지 않은 tag, 그리고 문자열 배열인 includeexclude를 검증하세요. 이 오류 경로도 테스트하세요.

수정 예시
 export function jsdoc(options: JsdocPluginOptions): RootagePlugin {
+  if (options?.target !== "ComponentSpec") {
+    throw new Error("jsdoc plugin: `target` must be `ComponentSpec`");
+  }
   if (typeof options?.text !== "string" || options.text === "") {
     throw new Error("jsdoc plugin: `text` must be a non-empty string");
   }
   if (
     options.tag !== undefined &&
-    (typeof options.tag !== "string" || options.tag.startsWith("@"))
+    (typeof options.tag !== "string" || options.tag === "" || options.tag.startsWith("@"))
   ) {
     throw new Error('jsdoc plugin: `tag` must be a tag name without "@"');
   }
+  for (const [name, ids] of Object.entries({
+    include: options.include,
+    exclude: options.exclude,
+  })) {
+    if (ids !== undefined && (!Array.isArray(ids) || ids.some((id) => typeof id !== "string"))) {
+      throw new Error(`jsdoc plugin: \`${name}\` must be an array of strings`);
+    }
+  }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export function jsdoc(options: JsdocPluginOptions): RootagePlugin {
if (typeof options?.text !== "string" || options.text === "") {
throw new Error("jsdoc plugin: `text` must be a non-empty string");
}
if (
options.tag !== undefined &&
(typeof options.tag !== "string" || options.tag.startsWith("@"))
) {
throw new Error('jsdoc plugin: `tag` must be a tag name without "@"');
}
export function jsdoc(options: JsdocPluginOptions): RootagePlugin {
if (options?.target !== "ComponentSpec") {
throw new Error("jsdoc plugin: `target` must be `ComponentSpec`");
}
if (typeof options?.text !== "string" || options.text === "") {
throw new Error("jsdoc plugin: `text` must be a non-empty string");
}
if (
options.tag !== undefined &&
(typeof options.tag !== "string" ||
options.tag === "" ||
options.tag.startsWith("@"))
) {
throw new Error('jsdoc plugin: `tag` must be a tag name without "@"');
}
for (const [name, ids] of Object.entries({
include: options.include,
exclude: options.exclude,
})) {
if (
ids !== undefined &&
(!Array.isArray(ids) ||
ids.some((id) => typeof id !== "string"))
) {
throw new Error(`jsdoc plugin: \`${name}\` must be an array of strings`);
}
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ecosystem/rootage/core/src/plugins/jsdoc.ts` around lines 36 - 45, [jsdoc]
팩토리의 옵션 런타임 검증을 확장하세요. `target`은 `"ComponentSpec"`만 허용하고, `tag`가 지정되면 비어 있지 않은
문자열이어야 하며, `include`와 `exclude`는 각각 문자열 배열인지 검증해 잘못된 값에서 즉시 오류를 발생시키세요. 해당 오류
경로를 테스트로 추가하고 유효한 옵션의 기존 동작은 유지하세요.

@junghyeonsu junghyeonsu self-assigned this Aug 10, 2026
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