@highpixel-co/palda-design-system 0.4.1 → 0.5.0
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/README.md +21 -2
- package/dist/guide/LICENSE +21 -0
- package/dist/guide/NOTICE +29 -0
- package/dist/guide/README.md +57 -0
- package/dist/guide/catalog/components.yml +272 -0
- package/dist/guide/catalog/drafts.yml +333 -0
- package/dist/guide/catalog/icons.yml +309 -0
- package/dist/guide/catalog/patterns.yml +124 -0
- package/dist/guide/catalog/tokens.yml +14 -0
- package/dist/guide/docs/ACCESSIBILITY.md +10 -0
- package/dist/guide/docs/AI_UI_DESIGNER_HANDOFF.md +256 -0
- package/dist/guide/docs/COMPONENT_POLICY.md +105 -0
- package/dist/guide/docs/CONSUMER_GUIDE.md +131 -0
- package/dist/guide/docs/CONTENT.md +148 -0
- package/dist/guide/docs/DESIGN_GRAMMAR.md +378 -0
- package/dist/guide/docs/DESIGN_PRINCIPLES.md +254 -0
- package/dist/guide/docs/FIGMA_ALIGNMENT_DELTA.md +141 -0
- package/dist/guide/docs/FIGMA_NAME_MAPPING.md +84 -0
- package/dist/guide/docs/FIGMA_WORKFLOW.md +33 -0
- package/dist/guide/docs/ICON_POLICY.md +175 -0
- package/dist/guide/docs/LAYOUT.md +221 -0
- package/dist/guide/docs/PATTERN_POLICY.md +13 -0
- package/dist/guide/docs/TOKEN_POLICY.md +255 -0
- package/dist/guide/icons/manifest.json +572 -0
- package/dist/harness/check.mjs +330 -0
- package/dist/harness/cli.mjs +88 -0
- package/dist/harness/metadata.json +1323 -0
- package/dist/scripts/check-examples.mjs +444 -0
- package/package.json +13 -2
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
import { existsSync, lstatSync, readFileSync, readdirSync } from 'node:fs';
|
|
2
|
+
import { dirname, isAbsolute, join, relative, resolve } from 'node:path';
|
|
3
|
+
import yaml from 'js-yaml';
|
|
4
|
+
import ts from 'typescript';
|
|
5
|
+
import { validateScreenHtml } from '../scripts/check-examples.mjs';
|
|
6
|
+
|
|
7
|
+
const PACKAGE = '@highpixel-co/palda-design-system';
|
|
8
|
+
const EXCEPTION_RULES = ['primitive-import', 'icon-import', 'opaque-import'];
|
|
9
|
+
const SOURCE = /\.(?:[cm]?[jt]s|[jt]sx)$/;
|
|
10
|
+
const EXCLUDED = /(?:\.(?:test|spec|stories|gen)\.[cm]?[jt]sx?|\.d\.[cm]?ts)$/;
|
|
11
|
+
const IGNORED_DIRS = new Set(['node_modules', '.git', 'dist', 'coverage']);
|
|
12
|
+
const normalize = (path) => path.replaceAll('\\', '/');
|
|
13
|
+
|
|
14
|
+
function localPath(root, path) {
|
|
15
|
+
if (typeof path !== 'string' || !path.trim() || isAbsolute(path)) {
|
|
16
|
+
throw new Error(`프로젝트 내부 상대 경로가 필요합니다: ${path}`);
|
|
17
|
+
}
|
|
18
|
+
const target = resolve(root, path);
|
|
19
|
+
if (relative(root, target).startsWith('..') || isAbsolute(relative(root, target))) {
|
|
20
|
+
throw new Error(`프로젝트 밖 경로는 지원하지 않습니다: ${path}`);
|
|
21
|
+
}
|
|
22
|
+
// 부모 디렉터리의 symlink도 외부 경로를 숨기지 못하게 한다.
|
|
23
|
+
let cursor = target;
|
|
24
|
+
while (cursor !== root) {
|
|
25
|
+
if (existsSync(cursor) && lstatSync(cursor).isSymbolicLink()) {
|
|
26
|
+
throw new Error(`symlink 경로는 지원하지 않습니다: ${path}`);
|
|
27
|
+
}
|
|
28
|
+
cursor = dirname(cursor);
|
|
29
|
+
}
|
|
30
|
+
return target;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function validateConfig(root, config) {
|
|
34
|
+
if (!config || typeof config !== 'object' || Array.isArray(config)) {
|
|
35
|
+
throw new Error('설정은 JSON 객체여야 합니다.');
|
|
36
|
+
}
|
|
37
|
+
for (const key of Object.keys(config)) {
|
|
38
|
+
if (!['source', 'screens', 'exceptions'].includes(key))
|
|
39
|
+
throw new Error(`알 수 없는 설정: ${key}`);
|
|
40
|
+
}
|
|
41
|
+
for (const key of ['source', 'screens']) {
|
|
42
|
+
if (config[key] === undefined) continue;
|
|
43
|
+
if (!Array.isArray(config[key]) || config[key].length === 0)
|
|
44
|
+
throw new Error(`${key} 경로가 없습니다.`);
|
|
45
|
+
config[key].forEach((path) => localPath(root, path));
|
|
46
|
+
}
|
|
47
|
+
if (config.exceptions !== undefined && !Array.isArray(config.exceptions)) {
|
|
48
|
+
throw new Error('exceptions는 배열이어야 합니다.');
|
|
49
|
+
}
|
|
50
|
+
for (const entry of config.exceptions ?? []) {
|
|
51
|
+
if (
|
|
52
|
+
!entry ||
|
|
53
|
+
typeof entry !== 'object' ||
|
|
54
|
+
Object.keys(entry).some((key) => !['file', 'rule', 'symbol', 'reason'].includes(key)) ||
|
|
55
|
+
!EXCEPTION_RULES.includes(entry.rule) ||
|
|
56
|
+
!['file', 'symbol', 'reason'].every(
|
|
57
|
+
(key) => typeof entry[key] === 'string' && entry[key].trim(),
|
|
58
|
+
) ||
|
|
59
|
+
!SOURCE.test(entry.file) ||
|
|
60
|
+
/[*?]/.test(entry.file)
|
|
61
|
+
) {
|
|
62
|
+
throw new Error('예외는 정확한 file, rule, symbol과 reason을 지정해야 합니다.');
|
|
63
|
+
}
|
|
64
|
+
localPath(root, entry.file);
|
|
65
|
+
if (normalize(entry.file) !== normalize(relative(root, resolve(root, entry.file)))) {
|
|
66
|
+
throw new Error(`예외 file은 정규화된 상대 경로여야 합니다: ${entry.file}`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return config;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function collect(root, paths, accepts) {
|
|
73
|
+
const files = new Set();
|
|
74
|
+
let excluded = 0;
|
|
75
|
+
function visit(directory) {
|
|
76
|
+
if (!existsSync(directory) || !lstatSync(directory).isDirectory()) {
|
|
77
|
+
throw new Error(`검사 디렉터리가 없습니다: ${relative(root, directory)}`);
|
|
78
|
+
}
|
|
79
|
+
for (const entry of readdirSync(directory, { withFileTypes: true })) {
|
|
80
|
+
if (IGNORED_DIRS.has(entry.name)) continue;
|
|
81
|
+
const file = join(directory, entry.name);
|
|
82
|
+
if (entry.isSymbolicLink())
|
|
83
|
+
throw new Error(`symlink 대상은 지원하지 않습니다: ${relative(root, file)}`);
|
|
84
|
+
if (entry.isDirectory()) visit(file);
|
|
85
|
+
else if (accepts(entry.name)) {
|
|
86
|
+
if (EXCLUDED.test(entry.name)) excluded++;
|
|
87
|
+
else files.add(file);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
paths.forEach((path) => visit(localPath(root, path)));
|
|
92
|
+
if (files.size === 0)
|
|
93
|
+
throw new Error('검사 대상 파일이 0개입니다. 설정 경로와 지원 확장자를 확인하세요.');
|
|
94
|
+
return { files: [...files].sort(), excluded };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function lockedDependency(root) {
|
|
98
|
+
let directory = root;
|
|
99
|
+
while (true) {
|
|
100
|
+
const pnpm = join(directory, 'pnpm-lock.yaml');
|
|
101
|
+
const npm = join(directory, 'package-lock.json');
|
|
102
|
+
if (existsSync(pnpm) && existsSync(npm))
|
|
103
|
+
throw new Error('잠금 파일이 둘입니다. 프로젝트의 패키지 매니저를 하나로 정하세요.');
|
|
104
|
+
const importer = normalize(relative(directory, root));
|
|
105
|
+
if (existsSync(pnpm)) {
|
|
106
|
+
const lock = yaml.load(readFileSync(pnpm, 'utf8'));
|
|
107
|
+
if (String(lock?.lockfileVersion) !== '9.0' && String(lock?.lockfileVersion) !== '9') {
|
|
108
|
+
throw new Error('pnpm lockfileVersion 9만 지원합니다.');
|
|
109
|
+
}
|
|
110
|
+
const entry = lock.importers?.[importer || '.'];
|
|
111
|
+
const dep = entry?.dependencies?.[PACKAGE] ?? entry?.devDependencies?.[PACKAGE];
|
|
112
|
+
return {
|
|
113
|
+
specifier: dep?.specifier,
|
|
114
|
+
version: typeof dep?.version === 'string' ? dep.version.split('(')[0] : dep?.version,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
if (existsSync(npm)) {
|
|
118
|
+
const lock = JSON.parse(readFileSync(npm, 'utf8'));
|
|
119
|
+
if (![2, 3].includes(lock.lockfileVersion))
|
|
120
|
+
throw new Error('npm lockfileVersion 2 또는 3만 지원합니다.');
|
|
121
|
+
const entry = lock.packages?.[importer];
|
|
122
|
+
let prefix = importer;
|
|
123
|
+
let installed;
|
|
124
|
+
while (true) {
|
|
125
|
+
installed = lock.packages?.[`${prefix ? `${prefix}/` : ''}node_modules/${PACKAGE}`];
|
|
126
|
+
if (installed || !prefix) break;
|
|
127
|
+
const parent = normalize(dirname(prefix));
|
|
128
|
+
prefix = parent === '.' ? '' : parent;
|
|
129
|
+
}
|
|
130
|
+
return {
|
|
131
|
+
specifier: entry?.dependencies?.[PACKAGE] ?? entry?.devDependencies?.[PACKAGE],
|
|
132
|
+
version: installed?.version,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
if (existsSync(join(directory, 'yarn.lock')))
|
|
136
|
+
throw new Error('Yarn 잠금 파일은 아직 지원하지 않습니다.');
|
|
137
|
+
const parent = dirname(directory);
|
|
138
|
+
if (parent === directory) throw new Error('pnpm-lock.yaml 또는 package-lock.json이 없습니다.');
|
|
139
|
+
directory = parent;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function dependencyProblems(root, metadata) {
|
|
144
|
+
try {
|
|
145
|
+
const manifest = JSON.parse(readFileSync(join(root, 'package.json'), 'utf8'));
|
|
146
|
+
const version = manifest.dependencies?.[PACKAGE] ?? manifest.devDependencies?.[PACKAGE];
|
|
147
|
+
if (typeof version !== 'string' || !/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(version)) {
|
|
148
|
+
throw new Error(`DS 의존성은 정확한 버전으로 고정해야 합니다: ${version ?? '없음'}`);
|
|
149
|
+
}
|
|
150
|
+
if (version !== metadata.packageVersion)
|
|
151
|
+
throw new Error(
|
|
152
|
+
`앱 DS 버전 ${version}과 실행 중인 하네스 ${metadata.packageVersion}이 다릅니다.`,
|
|
153
|
+
);
|
|
154
|
+
const locked = lockedDependency(root);
|
|
155
|
+
if (locked.specifier !== version || locked.version !== version) {
|
|
156
|
+
throw new Error(
|
|
157
|
+
`DS package.json과 lockfile 버전이 다릅니다: ${version} / ${locked.specifier} / ${locked.version}`,
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
return [];
|
|
161
|
+
} catch (error) {
|
|
162
|
+
return [
|
|
163
|
+
{
|
|
164
|
+
file: 'package.json',
|
|
165
|
+
line: 1,
|
|
166
|
+
rule: 'dependency',
|
|
167
|
+
symbol: PACKAGE,
|
|
168
|
+
message: error.message,
|
|
169
|
+
},
|
|
170
|
+
];
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export function checkConsumer(projectRoot, config, metadata) {
|
|
175
|
+
const root = resolve(projectRoot);
|
|
176
|
+
validateConfig(root, config);
|
|
177
|
+
const scan = collect(root, config.source ?? ['src'], (name) => SOURCE.test(name));
|
|
178
|
+
const report = {
|
|
179
|
+
files: scan.files.length,
|
|
180
|
+
excluded: scan.excluded,
|
|
181
|
+
exceptions: 0,
|
|
182
|
+
drafts: [],
|
|
183
|
+
problems: dependencyProblems(root, metadata),
|
|
184
|
+
};
|
|
185
|
+
function add(problem) {
|
|
186
|
+
if (
|
|
187
|
+
(config.exceptions ?? []).some(
|
|
188
|
+
(entry) =>
|
|
189
|
+
entry.file === problem.file &&
|
|
190
|
+
entry.rule === problem.rule &&
|
|
191
|
+
entry.symbol === problem.symbol,
|
|
192
|
+
)
|
|
193
|
+
)
|
|
194
|
+
report.exceptions++;
|
|
195
|
+
else report.problems.push(problem);
|
|
196
|
+
}
|
|
197
|
+
for (const fullPath of scan.files) {
|
|
198
|
+
const file = normalize(relative(root, fullPath));
|
|
199
|
+
const parsed = ts.createSourceFile(
|
|
200
|
+
fullPath,
|
|
201
|
+
readFileSync(fullPath, 'utf8'),
|
|
202
|
+
ts.ScriptTarget.Latest,
|
|
203
|
+
true,
|
|
204
|
+
);
|
|
205
|
+
for (const diagnostic of parsed.parseDiagnostics) {
|
|
206
|
+
add({
|
|
207
|
+
file,
|
|
208
|
+
line: parsed.getLineAndCharacterOfPosition(diagnostic.start ?? 0).line + 1,
|
|
209
|
+
rule: 'syntax',
|
|
210
|
+
symbol: '',
|
|
211
|
+
message: ts.flattenDiagnosticMessageText(diagnostic.messageText, '\n'),
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
function inspect(node, moduleName, names, opaque = false) {
|
|
215
|
+
const line = parsed.getLineAndCharacterOfPosition(node.getStart(parsed)).line + 1;
|
|
216
|
+
const problem = (rule, symbol, message) => add({ file, line, rule, symbol, message });
|
|
217
|
+
if (moduleName.startsWith('@radix-ui/') || moduleName === 'class-variance-authority') {
|
|
218
|
+
problem(
|
|
219
|
+
'primitive-import',
|
|
220
|
+
moduleName,
|
|
221
|
+
`${moduleName} 직접 사용 대신 DS 자산을 사용하세요.`,
|
|
222
|
+
);
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
const isDs = moduleName === PACKAGE || moduleName.startsWith(`${PACKAGE}/`);
|
|
226
|
+
if (!isDs && moduleName !== 'lucide-react') return;
|
|
227
|
+
if (moduleName === `${PACKAGE}/styles.css` && names.length === 0 && !opaque) return;
|
|
228
|
+
if (opaque) {
|
|
229
|
+
problem(
|
|
230
|
+
'opaque-import',
|
|
231
|
+
moduleName,
|
|
232
|
+
`${moduleName}: 이름을 명시한 정적 import/re-export를 사용하세요.`,
|
|
233
|
+
);
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
for (const { symbol, typeOnly } of names) {
|
|
237
|
+
if (moduleName === 'lucide-react') {
|
|
238
|
+
if (!typeOnly && metadata.iconReplacements[symbol])
|
|
239
|
+
problem(
|
|
240
|
+
'icon-import',
|
|
241
|
+
symbol,
|
|
242
|
+
`${symbol} 대신 DS ${metadata.iconReplacements[symbol]}을 사용하세요.`,
|
|
243
|
+
);
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
const subpath = moduleName === PACKAGE ? '.' : `.${moduleName.slice(PACKAGE.length)}`;
|
|
247
|
+
const group = metadata.exports[subpath];
|
|
248
|
+
if (
|
|
249
|
+
!group ||
|
|
250
|
+
!(typeOnly ? [...group.values, ...group.types] : group.values).includes(symbol)
|
|
251
|
+
) {
|
|
252
|
+
problem(
|
|
253
|
+
'unknown-export',
|
|
254
|
+
symbol,
|
|
255
|
+
`${moduleName}에 ${typeOnly ? '타입 ' : ''}${symbol} export가 없습니다.`,
|
|
256
|
+
);
|
|
257
|
+
} else if (!typeOnly) {
|
|
258
|
+
for (const asset of metadata.assets.filter(
|
|
259
|
+
(asset) => asset.status === 'draft' && asset.exports.includes(symbol),
|
|
260
|
+
)) {
|
|
261
|
+
report.drafts.push({ file, line, symbol, asset: asset.key });
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
if (isDs && names.length === 0 && moduleName !== `${PACKAGE}/styles.css`) {
|
|
266
|
+
const subpath = moduleName === PACKAGE ? '.' : `.${moduleName.slice(PACKAGE.length)}`;
|
|
267
|
+
if (!metadata.exports[subpath])
|
|
268
|
+
problem('unknown-export', moduleName, `지원하지 않는 DS 경로: ${moduleName}`);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
function visit(node) {
|
|
272
|
+
if (
|
|
273
|
+
(ts.isImportDeclaration(node) || ts.isExportDeclaration(node)) &&
|
|
274
|
+
node.moduleSpecifier &&
|
|
275
|
+
ts.isStringLiteral(node.moduleSpecifier)
|
|
276
|
+
) {
|
|
277
|
+
const clause = ts.isImportDeclaration(node) ? node.importClause : node;
|
|
278
|
+
const bindings = ts.isImportDeclaration(node) ? clause?.namedBindings : node.exportClause;
|
|
279
|
+
const names =
|
|
280
|
+
bindings && (ts.isNamedImports(bindings) || ts.isNamedExports(bindings))
|
|
281
|
+
? bindings.elements.map((element) => ({
|
|
282
|
+
symbol: (element.propertyName ?? element.name).text,
|
|
283
|
+
typeOnly: !!clause.isTypeOnly || element.isTypeOnly,
|
|
284
|
+
}))
|
|
285
|
+
: [];
|
|
286
|
+
if (ts.isImportDeclaration(node) && clause?.name)
|
|
287
|
+
names.unshift({ symbol: 'default', typeOnly: !!clause.isTypeOnly });
|
|
288
|
+
const opaque =
|
|
289
|
+
(!!bindings && ts.isNamespaceImport(bindings)) ||
|
|
290
|
+
(ts.isExportDeclaration(node) && (!bindings || ts.isNamespaceExport(bindings)));
|
|
291
|
+
inspect(node, node.moduleSpecifier.text, names, opaque);
|
|
292
|
+
} else if (
|
|
293
|
+
ts.isCallExpression(node) &&
|
|
294
|
+
(node.expression.kind === ts.SyntaxKind.ImportKeyword ||
|
|
295
|
+
(ts.isIdentifier(node.expression) && node.expression.text === 'require')) &&
|
|
296
|
+
node.arguments[0] &&
|
|
297
|
+
ts.isStringLiteralLike(node.arguments[0])
|
|
298
|
+
) {
|
|
299
|
+
inspect(node, node.arguments[0].text, [], true);
|
|
300
|
+
} else if (
|
|
301
|
+
ts.isImportEqualsDeclaration(node) &&
|
|
302
|
+
ts.isExternalModuleReference(node.moduleReference) &&
|
|
303
|
+
node.moduleReference.expression &&
|
|
304
|
+
ts.isStringLiteral(node.moduleReference.expression)
|
|
305
|
+
) {
|
|
306
|
+
inspect(node, node.moduleReference.expression.text, [], true);
|
|
307
|
+
}
|
|
308
|
+
ts.forEachChild(node, visit);
|
|
309
|
+
}
|
|
310
|
+
visit(parsed);
|
|
311
|
+
}
|
|
312
|
+
return report;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
export function checkScreens(projectRoot, config) {
|
|
316
|
+
const root = resolve(projectRoot);
|
|
317
|
+
validateConfig(root, config);
|
|
318
|
+
const scan = collect(root, config.screens ?? ['preview'], (name) => name.endsWith('.html'));
|
|
319
|
+
const problems = scan.files.flatMap((fullPath) => {
|
|
320
|
+
const file = normalize(relative(root, fullPath));
|
|
321
|
+
return validateScreenHtml(readFileSync(fullPath, 'utf8'), file, true).map((message) => ({
|
|
322
|
+
file,
|
|
323
|
+
line: 1,
|
|
324
|
+
rule: 'screen',
|
|
325
|
+
symbol: '',
|
|
326
|
+
message,
|
|
327
|
+
}));
|
|
328
|
+
});
|
|
329
|
+
return { files: scan.files.length, excluded: scan.excluded, exceptions: 0, drafts: [], problems };
|
|
330
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
3
|
+
import { dirname, join, resolve } from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { parseArgs } from 'node:util';
|
|
6
|
+
import { checkConsumer, checkScreens } from './check.mjs';
|
|
7
|
+
|
|
8
|
+
const directory = dirname(fileURLToPath(import.meta.url));
|
|
9
|
+
let json = process.argv.includes('--json');
|
|
10
|
+
try {
|
|
11
|
+
const { values, positionals } = parseArgs({
|
|
12
|
+
allowPositionals: true,
|
|
13
|
+
options: {
|
|
14
|
+
root: { type: 'string' },
|
|
15
|
+
config: { type: 'string' },
|
|
16
|
+
json: { type: 'boolean' },
|
|
17
|
+
help: { type: 'boolean', short: 'h' },
|
|
18
|
+
version: { type: 'boolean' },
|
|
19
|
+
},
|
|
20
|
+
});
|
|
21
|
+
json = values.json ?? false;
|
|
22
|
+
if (values.help || (positionals.length === 0 && !values.version)) {
|
|
23
|
+
console.log(`palda-ds check|screens|guide [--root path] [--config path] [--json]
|
|
24
|
+
check React/TypeScript 앱의 DS import와 package/lockfile 버전 검사
|
|
25
|
+
screens HTML 화면 선언과 블록 규칙 검사 (기본 preview/)
|
|
26
|
+
guide 설치된 사용 설명서 위치
|
|
27
|
+
--version 설치된 하네스/DS 버전
|
|
28
|
+
설정: 앱의 palda-ds.config.json (선택). 대상 0개·구성 오류는 실패합니다.`);
|
|
29
|
+
} else {
|
|
30
|
+
const metadata = JSON.parse(readFileSync(join(directory, 'metadata.json'), 'utf8'));
|
|
31
|
+
if (
|
|
32
|
+
metadata.schemaVersion !== 1 ||
|
|
33
|
+
!metadata.packageVersion ||
|
|
34
|
+
!metadata.exports ||
|
|
35
|
+
!metadata.assets ||
|
|
36
|
+
!metadata.iconReplacements
|
|
37
|
+
) {
|
|
38
|
+
throw new Error('패키지 하네스 metadata가 올바르지 않습니다. 패키지를 다시 설치하세요.');
|
|
39
|
+
}
|
|
40
|
+
if (values.version) {
|
|
41
|
+
console.log(metadata.packageVersion);
|
|
42
|
+
} else {
|
|
43
|
+
if (positionals.length !== 1 || !['check', 'screens', 'guide'].includes(positionals[0])) {
|
|
44
|
+
throw new Error('명령은 check, screens, guide 중 하나입니다. --help를 확인하세요.');
|
|
45
|
+
}
|
|
46
|
+
const command = positionals[0];
|
|
47
|
+
if (command === 'guide') {
|
|
48
|
+
const guide = resolve(directory, '../guide/docs/CONSUMER_GUIDE.md');
|
|
49
|
+
if (!existsSync(guide)) throw new Error('패키지 사용 설명서가 없습니다.');
|
|
50
|
+
console.log(json ? JSON.stringify({ version: metadata.packageVersion, guide }) : guide);
|
|
51
|
+
} else {
|
|
52
|
+
const root = resolve(values.root ?? process.cwd());
|
|
53
|
+
const configPath = resolve(root, values.config ?? 'palda-ds.config.json');
|
|
54
|
+
const config = existsSync(configPath) ? JSON.parse(readFileSync(configPath, 'utf8')) : {};
|
|
55
|
+
if (values.config && !existsSync(configPath))
|
|
56
|
+
throw new Error(`설정 파일이 없습니다: ${configPath}`);
|
|
57
|
+
const report =
|
|
58
|
+
command === 'check' ? checkConsumer(root, config, metadata) : checkScreens(root, config);
|
|
59
|
+
const output = {
|
|
60
|
+
command,
|
|
61
|
+
version: metadata.packageVersion,
|
|
62
|
+
ok: report.problems.length === 0,
|
|
63
|
+
...report,
|
|
64
|
+
};
|
|
65
|
+
if (json) console.log(JSON.stringify(output, null, 2));
|
|
66
|
+
else {
|
|
67
|
+
console.log(
|
|
68
|
+
`Palda DS ${metadata.packageVersion} ${command}: ${report.files}개 파일 검사, ${report.excluded}개 제외, ${report.exceptions}개 예외 적용`,
|
|
69
|
+
);
|
|
70
|
+
report.problems.forEach((p) =>
|
|
71
|
+
console.error(`${p.file}:${p.line} [${p.rule}] ${p.message}`),
|
|
72
|
+
);
|
|
73
|
+
report.drafts.forEach((d) =>
|
|
74
|
+
console.log(
|
|
75
|
+
`${d.file}:${d.line} [draft] ${d.symbol} — catalog/${d.asset}의 사용 조건을 검토하세요.`,
|
|
76
|
+
),
|
|
77
|
+
);
|
|
78
|
+
console.log(output.ok ? '검사 통과' : `검사 실패 ${report.problems.length}건`);
|
|
79
|
+
}
|
|
80
|
+
if (!output.ok) process.exitCode = 1;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
} catch (error) {
|
|
85
|
+
if (json) console.log(JSON.stringify({ ok: false, error: error.message }));
|
|
86
|
+
else console.error(`Palda DS 검사 구성 오류: ${error.message}`);
|
|
87
|
+
process.exitCode = 1;
|
|
88
|
+
}
|