@moloodev/platform 0.10.0 → 0.11.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@moloodev/platform",
3
- "version": "0.10.0",
3
+ "version": "0.11.0",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": "./src/index.ts",
@@ -21,9 +21,9 @@
21
21
  },
22
22
  "devDependencies": {
23
23
  "@types/node": "^24.13.3",
24
- "tsx": "^4.23.13",
24
+ "vitest": "5.1.0",
25
25
  "typescript": "^7.0.2",
26
- "@moloodev/typescript-config": "0.10.0"
26
+ "@moloodev/typescript-config": "0.11.0"
27
27
  },
28
28
  "publishConfig": {
29
29
  "access": "public"
@@ -35,6 +35,6 @@
35
35
  ],
36
36
  "scripts": {
37
37
  "typecheck": "tsc --noEmit",
38
- "test": "tsx --test 'src/**/*.test.ts' 'src/**/*.test.tsx'"
38
+ "test": "vitest run --root ../.. --project @moloodev/platform"
39
39
  }
40
40
  }
@@ -1,25 +1,5 @@
1
- /** Icons used by asset kinds and file appearance resolvers. Independent of chrome `AppIcon`. */
2
- export const FILE_APPEARANCE_ICONS = [
3
- "tabler:folder",
4
- "tabler:hierarchy",
5
- "tabler:layout",
6
- "tabler:table",
7
- "tabler:markdown",
8
- "tabler:photo",
9
- "tabler:movie",
10
- "tabler:music",
11
- "tabler:file-text",
12
- "tabler:database",
13
- "tabler:code",
14
- "tabler:archive",
15
- "tabler:typography",
16
- "tabler:cube",
17
- "tabler:file",
18
- "tabler:package",
19
- "tabler:help",
20
- ] as const;
21
-
22
- export type AssetTypeIcon = (typeof FILE_APPEARANCE_ICONS)[number];
1
+ /** Tabler URI for a file glyph; hosts use a neutral glyph for unknown names. */
2
+ export type AssetTypeIcon = `tabler:${string}`;
23
3
 
24
4
  /**
25
5
  * Primitive storage and CRDT collaboration mode for an asset.
@@ -56,6 +36,8 @@ export type FileAppearance = {
56
36
 
57
37
  /** Optional file matcher rule declared by an installed `EditorManifest`. */
58
38
  export type FileAppearanceMatcher = {
39
+ /** Explicit asset kind id for type-only surfaces; never inferred from an editor. */
40
+ type?: string;
59
41
  extensions?: readonly string[];
60
42
  mimeTypes?: readonly string[];
61
43
  schema?: string;
@@ -71,31 +53,32 @@ export const UNKNOWN_FILE_APPEARANCE: FileAppearance = {
71
53
  icon: "tabler:help",
72
54
  };
73
55
 
74
- const PATTERN_APPEARANCE_MAP = {
75
- "moloo.folder": { label: "폴더", color: "#94a3b8", icon: "tabler:folder" },
76
- "moloo.loop": { label: "루프", color: "#f07858", icon: "tabler:hierarchy" },
77
- "moloo.table": { label: "테이블", color: "#3d9b8f", icon: "tabler:table" },
78
- "moloo.markdown": { label: "마크다운", color: "#5b6abf", icon: "tabler:markdown" },
79
- "moloo.bundle": { label: "번들", color: "#2f7a86", icon: "tabler:package" },
80
- "moloo.layout": { label: "레이아웃", color: "#7695b3", icon: "tabler:layout" },
81
- "moloo.catalog": { label: "카탈로그", color: "#9474ad", icon: "tabler:package" },
82
- "moloo.image": { label: "이미지", color: "#a5c65b", icon: "tabler:photo" },
83
- "moloo.video": { label: "비디오", color: "#8f7fe0", icon: "tabler:movie" },
84
- "moloo.audio": { label: "오디오", color: "#c78cd9", icon: "tabler:music" },
85
- "moloo.document": { label: "문서", color: "#89b4e8", icon: "tabler:file-text" },
86
- "moloo.data": { label: "데이터", color: "#66b06f", icon: "tabler:database" },
87
- "moloo.code": { label: "코드", color: "#5c73c9", icon: "tabler:code" },
88
- "moloo.archive": { label: "압축", color: "#b3a27a", icon: "tabler:archive" },
89
- "moloo.font": { label: "글꼴", color: "#b09bbf", icon: "tabler:typography" },
90
- "moloo.model": { label: "3D 모델", color: "#d1b24f", icon: "tabler:cube" },
91
- "moloo.binary": { label: "바이너리", color: "#a7adba", icon: "tabler:file" },
92
- } as const satisfies Record<string, FileAppearance>;
56
+ /** Classification identifiers, independent of display identity. */
57
+ export const KNOWN_PATTERN_IDS = [
58
+ "moloo.folder", "moloo.loop", "moloo.table", "moloo.markdown",
59
+ "moloo.bundle", "moloo.layout", "moloo.catalog", "moloo.image",
60
+ "moloo.video", "moloo.audio", "moloo.document", "moloo.data",
61
+ "moloo.code", "moloo.archive", "moloo.font", "moloo.model", "moloo.binary",
62
+ ] as const;
63
+ export type KnownPatternId = (typeof KNOWN_PATTERN_IDS)[number];
93
64
 
94
- export type KnownPatternId = keyof typeof PATTERN_APPEARANCE_MAP;
65
+ const DIRECTORY_APPEARANCE: FileAppearance = {
66
+ label: "폴더", color: UNKNOWN_FILE_APPEARANCE.color, icon: "tabler:folder",
67
+ };
95
68
 
96
- export const KNOWN_PATTERN_IDS = Object.keys(
97
- PATTERN_APPEARANCE_MAP,
98
- ) as KnownPatternId[];
69
+ /** Type-only surfaces use the same explicit file-type declaration as file surfaces. */
70
+ export function resolveTypeAppearance(
71
+ type: string,
72
+ matchers: readonly FileAppearanceMatcher[] = [],
73
+ ): FileAppearance {
74
+ if (type === "moloo.folder") return DIRECTORY_APPEARANCE;
75
+ const matcher = matchers.find((entry) => entry.type === type);
76
+ return {
77
+ label: matcher?.label ?? (type || UNKNOWN_FILE_APPEARANCE.label),
78
+ icon: matcher?.icon ?? UNKNOWN_FILE_APPEARANCE.icon,
79
+ color: matcher?.color ?? UNKNOWN_FILE_APPEARANCE.color,
80
+ };
81
+ }
99
82
 
100
83
  const DOCUMENT_SUBTYPES = new Set([
101
84
  "pdf",
@@ -340,14 +323,6 @@ export function classifyFile(input: {
340
323
  };
341
324
  }
342
325
 
343
- const FIRST_PARTY_SCHEMA_APPEARANCE: Record<string, FileAppearance> = {
344
- [FIRST_PARTY_SCHEMAS.layout]: PATTERN_APPEARANCE_MAP["moloo.layout"],
345
- [FIRST_PARTY_SCHEMAS.catalog]: PATTERN_APPEARANCE_MAP["moloo.catalog"],
346
- [FIRST_PARTY_SCHEMAS.table]: PATTERN_APPEARANCE_MAP["moloo.table"],
347
- [FIRST_PARTY_SCHEMAS.loop]: PATTERN_APPEARANCE_MAP["moloo.loop"],
348
- [FIRST_PARTY_SCHEMAS.bundle]: PATTERN_APPEARANCE_MAP["moloo.bundle"],
349
- };
350
-
351
326
  function isCompoundExtension(ext: string): boolean {
352
327
  const withoutLeadingDot = ext.startsWith(".") ? ext.slice(1) : ext;
353
328
  return withoutLeadingDot.includes(".");
@@ -365,7 +340,8 @@ function scoreMatcher(
365
340
  if (matcher.storageMode && file.storageMode && matcher.storageMode !== file.storageMode) {
366
341
  return 0;
367
342
  }
368
- if (matcher.schema && file.schema && matcher.schema === file.schema) {
343
+ if (matcher.schema && file.schema) {
344
+ if (matcher.schema !== file.schema) return 0;
369
345
  return 400;
370
346
  }
371
347
  const lowerName = file.name.toLowerCase();
@@ -396,119 +372,42 @@ function scoreMatcher(
396
372
  }
397
373
 
398
374
  /**
399
- * Resolve cosmetic display metadata (`label`, `color`, `icon`) for a file from
400
- * installed editor matchers, falling back to first-party `$schema`/extension
401
- * rules and OS-style MIME family defaults.
375
+ * File identity comes only from manifest fileTypes. Highest specificity wins:
376
+ * schema, compound extension, extension, exact MIME, MIME family. Ties keep the
377
+ * first declaration (hosts sort manifests by editor id). Missing fields are
378
+ * neutral, never borrowed from a different rule or the owning editor.
402
379
  */
403
380
  export function resolveFileAppearance(
404
381
  file: {
405
382
  name: string;
406
383
  storageMode?: string;
407
- contentType: string;
384
+ contentType?: string | null;
408
385
  schema?: string | null;
409
386
  },
410
387
  matchers: readonly FileAppearanceMatcher[] = [],
411
388
  ): FileAppearance {
412
389
  if (file.name === ".keep" || normalizeMimeType(file.contentType) === "application/x-directory") {
413
- return PATTERN_APPEARANCE_MAP["moloo.folder"];
390
+ return DIRECTORY_APPEARANCE;
414
391
  }
415
-
416
- const classified = classifyFile({
417
- fileName: file.name,
418
- contentType: file.contentType,
419
- });
420
- const effectiveSchema = file.schema ?? classified.defaultSchema;
392
+ const classified = classifyFile({ fileName: file.name, contentType: file.contentType });
421
393
  const effectiveFile = {
422
394
  ...file,
423
395
  storageMode: file.storageMode ?? classified.storageMode,
424
396
  contentType: normalizeMimeType(file.contentType) || classified.contentType,
425
- schema: effectiveSchema,
397
+ schema: file.schema ?? classified.defaultSchema,
426
398
  };
427
-
428
399
  let bestScore = 0;
429
- let bestMatcher: FileAppearanceMatcher | null = null;
400
+ let bestMatcher: FileAppearanceMatcher | undefined;
430
401
  for (const matcher of matchers) {
431
402
  const score = scoreMatcher(effectiveFile, matcher);
432
- if (score > bestScore && (matcher.label || matcher.icon || matcher.color)) {
403
+ if (score > bestScore) {
433
404
  bestScore = score;
434
405
  bestMatcher = matcher;
435
406
  }
436
407
  }
437
-
438
- if (effectiveSchema && FIRST_PARTY_SCHEMA_APPEARANCE[effectiveSchema]) {
439
- const base = FIRST_PARTY_SCHEMA_APPEARANCE[effectiveSchema]!;
440
- // Only let a matcher override a first-party schema/compound-extension appearance
441
- // when it matched at schema (>= 400) or compound-extension (>= 300) specificity,
442
- // never on a generic .json or application/json rule.
443
- const override = bestScore >= 300 ? bestMatcher : null;
444
- return {
445
- label: override?.label ?? base.label,
446
- color: override?.color ?? base.color,
447
- icon: override?.icon ?? base.icon,
448
- };
449
- }
450
-
451
- const lowerName = file.name.toLowerCase();
452
- const mime = effectiveFile.contentType;
453
- if (
454
- mime === "text/markdown" ||
455
- lowerName.endsWith(".md") ||
456
- lowerName.endsWith(".markdown")
457
- ) {
458
- const md = PATTERN_APPEARANCE_MAP["moloo.markdown"];
459
- const override = bestScore >= 150 ? bestMatcher : null;
460
- return {
461
- label: override?.label ?? md.label,
462
- color: override?.color ?? md.color,
463
- icon: override?.icon ?? md.icon,
464
- };
465
- }
466
-
467
- const fallbackKind = patternForContentType(mime);
468
- const fallback = PATTERN_APPEARANCE_MAP[fallbackKind];
469
408
  return {
470
- label: bestMatcher?.label ?? fallback.label,
471
- color: bestMatcher?.color ?? fallback.color,
472
- icon: bestMatcher?.icon ?? fallback.icon,
409
+ label: bestMatcher?.label ?? UNKNOWN_FILE_APPEARANCE.label,
410
+ icon: bestMatcher?.icon ?? UNKNOWN_FILE_APPEARANCE.icon,
411
+ color: bestMatcher?.color ?? UNKNOWN_FILE_APPEARANCE.color,
473
412
  };
474
413
  }
475
-
476
- /**
477
- * Resolve cosmetic appearance for either a pattern/kind identifier (`moloo.*`)
478
- * or an asset summary with `name`, `storageMode`, `contentType`, and `schema`.
479
- * When file fields (`storageMode`, `schema`, or `contentType` + `name`) are
480
- * present, they take precedence over a legacy `type` string.
481
- */
482
- export function resolvePatternAppearance(
483
- input:
484
- | string
485
- | {
486
- type?: string;
487
- name?: string;
488
- storageMode?: StorageMode;
489
- contentType?: string | null;
490
- schema?: string | null;
491
- },
492
- ): FileAppearance {
493
- if (typeof input === "string") {
494
- return Object.hasOwn(PATTERN_APPEARANCE_MAP, input)
495
- ? PATTERN_APPEARANCE_MAP[input as KnownPatternId]
496
- : UNKNOWN_FILE_APPEARANCE;
497
- }
498
- if (
499
- input.storageMode ||
500
- input.schema ||
501
- (input.contentType && input.name)
502
- ) {
503
- return resolveFileAppearance({
504
- name: input.name ?? "",
505
- storageMode: input.storageMode,
506
- contentType: input.contentType ?? "application/octet-stream",
507
- schema: input.schema ?? null,
508
- });
509
- }
510
- if (input.type && Object.hasOwn(PATTERN_APPEARANCE_MAP, input.type)) {
511
- return PATTERN_APPEARANCE_MAP[input.type as KnownPatternId];
512
- }
513
- return UNKNOWN_FILE_APPEARANCE;
514
- }
@@ -0,0 +1,7 @@
1
+ /** Editor identity is declared at the manifest top level, independently of files. */
2
+ export function resolveEditorAppearance(editor: { icon?: string; color?: string }): {
3
+ icon: string;
4
+ color: string;
5
+ } {
6
+ return { icon: editor.icon ?? "tabler:help", color: editor.color ?? "#94a3b8" };
7
+ }
package/src/index.ts CHANGED
@@ -7,7 +7,6 @@ export {
7
7
  } from "./qualified-name";
8
8
 
9
9
  export {
10
- FILE_APPEARANCE_ICONS,
11
10
  FIRST_PARTY_SCHEMAS,
12
11
  KNOWN_PATTERN_IDS,
13
12
  STORAGE_MODES,
@@ -16,7 +15,7 @@ export {
16
15
  isStorageMode,
17
16
  patternForContentType,
18
17
  resolveFileAppearance,
19
- resolvePatternAppearance,
18
+ resolveTypeAppearance,
20
19
  type AssetTypeIcon,
21
20
  type FileAppearance,
22
21
  type FileAppearanceMatcher,
@@ -94,3 +93,5 @@ export { EDITOR_TRACKS, isEditorTrack, type EditorTrack } from "./editor-track";
94
93
  export type { PublishTarget } from "./publish-target";
95
94
  export { PUBLISH_ERROR_MESSAGES, publishErrorBody, parsePublishName } from "./publish-target";
96
95
  export { isPublisherHandle, isReservedPublisherHandle, reservedPublishers } from "./publisher-handle";
96
+
97
+ export { resolveEditorAppearance } from "./editor-appearance";