sh-ui-cli 0.98.0 → 0.98.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/data/changelog/versions.json +12 -0
- package/package.json +1 -1
- package/src/add.mjs +39 -14
- package/src/css-bundle.mjs +60 -0
- package/src/remove.mjs +10 -1
|
@@ -2,6 +2,18 @@
|
|
|
2
2
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
3
|
"$description": "sh-ui 릴리즈 노트 단일 소스. docs(React)와 showcase(Flutter)가 함께 읽는다. 새 릴리즈마다 맨 앞에 추가.",
|
|
4
4
|
"versions": [
|
|
5
|
+
{
|
|
6
|
+
"version": "0.98.1",
|
|
7
|
+
"date": "2026-05-19",
|
|
8
|
+
"title": "크로스컴포넌트 상대 import 를 aliases.components 로 재작성 — NodeNext 소비자 호환",
|
|
9
|
+
"type": "patch",
|
|
10
|
+
"highlights": [
|
|
11
|
+
"**크로스컴포넌트 import 가 이제 `aliases.components` 경유로 emit** — `sidebar`(→popover), `calendar`(→select), `date-picker`(→calendar), `markdown-editor`(→code-editor), `code-tabs`(→tabs/code-panel), `form-rhf`/`form-tanstack`/`form-yup`(→form) 등 형제 컴포넌트를 import 하던 모든 컴포넌트가, registry 의 `import … from \"../popover\"` 상대경로 대신 사용자 `sh-ui.config.json` 의 `aliases.components`(예: `@workspace/ui-core/components/popover`)로 재작성된다. `@SH_UI_UTILS@` → `aliases.utils` 치환과 동일한 add-time 변환.",
|
|
12
|
+
"**`moduleResolution: \"NodeNext\"`/`\"Node16\"` 소비자 깨짐 수정** — 상대경로/디렉터리 import 는 NodeNext 가 거부하고, 과거 일부 버전이 emit 한 `\"../popover/index.tsx\"` 의 명시적 `.tsx` 확장자는 TS5097 을 유발해 소비자가 `allowImportingTsExtensions`+`noEmit`(패키지 전체 declaration emit 비활성화) 같은 침습적 tsconfig 변경을 강요당했다. 이제 `\"Bundler\"`·`\"NodeNext\"` 양쪽에서 소비자 tsconfig 변경 없이 resolve 된다.",
|
|
13
|
+
"**add/remove 대칭 + registry 불변** — `remove` 의 unmodified 판정도 같은 재작성을 replay 해 형제-import 컴포넌트 삭제 시 '사용자 수정됨' false positive 가 나지 않는다. registry 소스·docs 듀얼카피·generated 산출물은 변경 없음(CLI 변환만). `aliases.components` 미설정 시 `@SH_UI_UTILS@` 와 동일한 톤의 친절 에러로 사전 안내. 회귀 단위 테스트 추가(전 스위트 407 통과, dual-copy drift 47 components 0)."
|
|
14
|
+
],
|
|
15
|
+
"url": "https://github.com/sanghyeonKim0201/sh-ui/releases/tag/v0.98.1"
|
|
16
|
+
},
|
|
5
17
|
{
|
|
6
18
|
"version": "0.98.0",
|
|
7
19
|
"date": "2026-05-16",
|
package/package.json
CHANGED
package/src/add.mjs
CHANGED
|
@@ -16,6 +16,8 @@ import {
|
|
|
16
16
|
stripStylesImport,
|
|
17
17
|
isStyleFile,
|
|
18
18
|
isTsxFile,
|
|
19
|
+
hasCrossComponentImport,
|
|
20
|
+
rewriteCrossComponentImports,
|
|
19
21
|
} from "./css-bundle.mjs";
|
|
20
22
|
|
|
21
23
|
/**
|
|
@@ -96,26 +98,49 @@ function resolveDest(template, config) {
|
|
|
96
98
|
}
|
|
97
99
|
|
|
98
100
|
/**
|
|
99
|
-
* registry source 안의 placeholder 를 사용자 config 값으로 치환.
|
|
100
|
-
* 현재 지원: `@SH_UI_UTILS@` → `aliases.utils` (예: `@/src/shared/lib/utils`).
|
|
101
|
+
* registry source 안의 placeholder / 상대 import 를 사용자 config 값으로 치환.
|
|
101
102
|
*
|
|
102
|
-
*
|
|
103
|
-
*
|
|
103
|
+
* 1) `@SH_UI_UTILS@` → `aliases.utils` (예: `@/src/shared/lib/utils`).
|
|
104
|
+
* registry 컴포넌트는 cn 유틸을 `import { cn } from "@SH_UI_UTILS@"` 로
|
|
105
|
+
* import 한다 — add 시점에 사용자 프로젝트 alias 로 치환해 module resolution 동작.
|
|
106
|
+
* 2) 크로스컴포넌트 상대 import (`from "../popover"` /
|
|
107
|
+
* `from "../popover/index.tsx"` / `from "../form/types"`) →
|
|
108
|
+
* `aliases.components` 경유 (예: `@workspace/ui-core/components/popover`).
|
|
109
|
+
* 상대경로/명시적 `.tsx` 확장자가 그대로 emit 되면 NodeNext 소비자가 깨지므로
|
|
110
|
+
* (TS5097 → `allowImportingTsExtensions`+`noEmit` 강요), utils 와 동일하게
|
|
111
|
+
* add 시점에 alias 로 정규화한다. 변환 규칙은 css-bundle.mjs 의
|
|
112
|
+
* rewriteCrossComponentImports 참조 (remove.mjs 가 동일 함수로 대칭 replay).
|
|
104
113
|
*
|
|
105
|
-
*
|
|
106
|
-
* 추가 후 import 깨진 것을 발견하기 전에
|
|
114
|
+
* 해당 alias 가 미설정인데 placeholder/상대 import 가 등장하면 친절 에러로
|
|
115
|
+
* 안내 — 사용자가 매 컴포넌트 추가 후 import 깨진 것을 발견하기 전에 잡는다.
|
|
107
116
|
*/
|
|
108
117
|
function substitutePlaceholders(content, config, srcRel) {
|
|
118
|
+
let out = content;
|
|
119
|
+
|
|
109
120
|
const PLACEHOLDER = "@SH_UI_UTILS@";
|
|
110
|
-
if (
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
121
|
+
if (out.includes(PLACEHOLDER)) {
|
|
122
|
+
const alias = config.aliases?.utils;
|
|
123
|
+
if (!alias) {
|
|
124
|
+
throw new Error(
|
|
125
|
+
`${srcRel} 가 cn 유틸을 import 합니다. sh-ui.config.json 에 aliases.utils 를 설정하세요.\n` +
|
|
126
|
+
` 예: "aliases": { "utils": "@/src/lib/utils" }`,
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
out = out.replaceAll(PLACEHOLDER, alias);
|
|
117
130
|
}
|
|
118
|
-
|
|
131
|
+
|
|
132
|
+
if (hasCrossComponentImport(out)) {
|
|
133
|
+
const componentsAlias = config.aliases?.components;
|
|
134
|
+
if (!componentsAlias) {
|
|
135
|
+
throw new Error(
|
|
136
|
+
`${srcRel} 가 다른 sh-ui 컴포넌트를 import 합니다. sh-ui.config.json 에 aliases.components 를 설정하세요.\n` +
|
|
137
|
+
` 예: "aliases": { "components": "@/src/components" }`,
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
out = rewriteCrossComponentImports(out, componentsAlias);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return out;
|
|
119
144
|
}
|
|
120
145
|
|
|
121
146
|
async function ensureDir(filePath) {
|
package/src/css-bundle.mjs
CHANGED
|
@@ -88,6 +88,66 @@ export function stripStylesImport(tsxText) {
|
|
|
88
88
|
);
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
+
/**
|
|
92
|
+
* 크로스컴포넌트 상대 import 매칭 정규식 생성 (호출마다 새 RegExp — lastIndex 상태 회피).
|
|
93
|
+
*
|
|
94
|
+
* 매칭: `... from "../<comp>..."` / `... from "../<comp>/index.tsx"` 형태.
|
|
95
|
+
* - group1: `from` 키워드 + 공백 (re-export `export {…} from` 도 포함)
|
|
96
|
+
* - group2: 따옴표
|
|
97
|
+
* - 리터럴 `../` 다음 negative-lookahead `(?!\.)` — `../../` / `../.x` 는 제외
|
|
98
|
+
* (sh-ui 컴포넌트는 항상 형제 디렉터리라 정확히 한 단계 `../`)
|
|
99
|
+
* - group3: 컴포넌트 경로 (따옴표/개행 없음, subpath 포함 가능: `form/types`)
|
|
100
|
+
* 같은 디렉터리(`./styles.css` 등)와 side-effect import(`import "..."`)는 매칭 안 됨.
|
|
101
|
+
*/
|
|
102
|
+
function crossComponentImportRe() {
|
|
103
|
+
return /(\bfrom\s+)(["'])\.\.\/(?!\.)([^"'\n]+)\2/g;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* 컴포넌트 소스에 다른 sh-ui 컴포넌트를 가리키는 상대 import 가 있는지.
|
|
108
|
+
* add 시 aliases.components 미설정을 사전 차단하는 게이트로 사용.
|
|
109
|
+
*/
|
|
110
|
+
export function hasCrossComponentImport(content) {
|
|
111
|
+
return crossComponentImportRe().test(content);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* registry 컴포넌트의 크로스컴포넌트 상대 import 를 사용자 config 의
|
|
116
|
+
* `aliases.components` 경유 모듈 스펙으로 재작성.
|
|
117
|
+
*
|
|
118
|
+
* 왜: registry 소스는 가독성을 위해 형제 컴포넌트를 `import … from "../popover"`
|
|
119
|
+
* 처럼 상대경로로 적는다. 이게 그대로 소비자 프로젝트에 emit 되면
|
|
120
|
+
* `moduleResolution: "NodeNext"`/`"Node16"` 환경에서 깨진다 — NodeNext 는
|
|
121
|
+
* 확장자 없는/디렉터리 상대 import 를 허용하지 않고, 과거 일부 버전이 emit 한
|
|
122
|
+
* `"../popover/index.tsx"` 는 명시적 `.tsx` 확장자라 TS5097 로 막힌다. 소비자는
|
|
123
|
+
* `allowImportingTsExtensions`+`noEmit` 같은 침습적 tsconfig 변경을 강요당한다.
|
|
124
|
+
* `@SH_UI_UTILS@` 가 `aliases.utils` 로 치환되는 것과 동일하게, 크로스컴포넌트
|
|
125
|
+
* import 도 add 시점에 `aliases.components` 로 재작성하면 Bundler/NodeNext
|
|
126
|
+
* 양쪽에서 소비자 tsconfig 변경 없이 resolve 된다.
|
|
127
|
+
*
|
|
128
|
+
* 변환 규칙: `../<rest>` → `<componentsAlias>/<rest>` (선행 `../` 제거),
|
|
129
|
+
* 그리고 말미의 `/index`(+`.tsx|.ts|.jsx|.js`)는 제거 — `../popover`,
|
|
130
|
+
* `../popover/index.tsx`, `../form/types` 가 모두 동일하게
|
|
131
|
+
* `<alias>/popover`, `<alias>/form/types` 로 정규화된다.
|
|
132
|
+
*
|
|
133
|
+
* `componentsAlias` 가 falsy 면 그대로 반환 (remove.mjs 의 best-effort 재생 replay
|
|
134
|
+
* 처럼 alias 미설정 컨텍스트에서 throw 하지 않기 위함 — 검증/차단은 add.mjs 책임).
|
|
135
|
+
*
|
|
136
|
+
* @param {string} content 컴포넌트 소스
|
|
137
|
+
* @param {string} componentsAlias 예: `@workspace/ui-core/components`
|
|
138
|
+
* @returns {string} 재작성된 소스
|
|
139
|
+
*/
|
|
140
|
+
export function rewriteCrossComponentImports(content, componentsAlias) {
|
|
141
|
+
if (!componentsAlias) return content;
|
|
142
|
+
return content.replace(
|
|
143
|
+
crossComponentImportRe(),
|
|
144
|
+
(_m, fromKw, quote, spec) => {
|
|
145
|
+
const rest = spec.replace(/\/index(\.[tj]sx?)?$/, "");
|
|
146
|
+
return `${fromKw}${quote}${componentsAlias}/${rest}${quote}`;
|
|
147
|
+
},
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
|
|
91
151
|
/**
|
|
92
152
|
* registry 의 file 엔트리가 CSS 변종인지 (bundled 모드에서 별도 처리 대상).
|
|
93
153
|
*/
|
package/src/remove.mjs
CHANGED
|
@@ -2,7 +2,13 @@ import { readFile, rm, rmdir, readdir, writeFile } from "node:fs/promises";
|
|
|
2
2
|
import { existsSync } from "node:fs";
|
|
3
3
|
import { dirname, resolve, relative } from "node:path";
|
|
4
4
|
import { getRegistryRoot } from "./paths.mjs";
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
removeSection,
|
|
7
|
+
isStyleFile,
|
|
8
|
+
stripStylesImport,
|
|
9
|
+
isTsxFile,
|
|
10
|
+
rewriteCrossComponentImports,
|
|
11
|
+
} from "./css-bundle.mjs";
|
|
6
12
|
|
|
7
13
|
/** registry file 엔트리가 현재 cssFramework 와 호환되는지 (add.mjs 와 동일 규칙). */
|
|
8
14
|
function frameworkMatches(entry, cssFramework) {
|
|
@@ -105,6 +111,9 @@ export async function remove({ cwd, names, force = false, dryRun = false }) {
|
|
|
105
111
|
if (config.aliases?.utils) {
|
|
106
112
|
out = out.replaceAll("@SH_UI_UTILS@", config.aliases.utils);
|
|
107
113
|
}
|
|
114
|
+
if (config.aliases?.components) {
|
|
115
|
+
out = rewriteCrossComponentImports(out, config.aliases.components);
|
|
116
|
+
}
|
|
108
117
|
if (bundled && isTsxFile(file)) out = stripStylesImport(out);
|
|
109
118
|
return out;
|
|
110
119
|
};
|