@open-mercato/ui 0.6.7-develop.6784.1.f80b9afce5 → 0.6.7-develop.6788.1.c2a1520a30

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.
@@ -0,0 +1,177 @@
1
+ import { execFileSync } from 'node:child_process'
2
+ import { readFileSync } from 'node:fs'
3
+ import { join } from 'node:path'
4
+ import { Project, ScriptKind, SyntaxKind, type ObjectLiteralExpression, type SourceFile } from 'ts-morph'
5
+ import { buildLucideRegistrySource } from '../../../../scripts/lucideRegistrySource.cjs'
6
+
7
+ const packageDir = join(__dirname, '..', '..', '..', '..')
8
+ const iconsDir = join(packageDir, 'src', 'backend', 'icons')
9
+
10
+ type ResolvedIcon = { kebab: string; exportName: string }
11
+
12
+ const populatedFixture: ResolvedIcon[] = [
13
+ { kebab: 'bell', exportName: 'Bell' },
14
+ { kebab: 'alert-circle', exportName: 'AlertCircle' },
15
+ { kebab: 'bar-chart-2', exportName: 'BarChart2' },
16
+ ]
17
+
18
+ const duplicateExportFixture: ResolvedIcon[] = [
19
+ { kebab: 'trash', exportName: 'Trash' },
20
+ { kebab: 'trash-can', exportName: 'Trash' },
21
+ ]
22
+
23
+ function parse(source: string): SourceFile {
24
+ const project = new Project({ useInMemoryFileSystem: true })
25
+ return project.createSourceFile('lucideRegistry.generated.tsx', source, {
26
+ overwrite: true,
27
+ scriptKind: ScriptKind.TSX,
28
+ })
29
+ }
30
+
31
+ function syntacticDiagnostics(source: string): string[] {
32
+ const sourceFile = parse(source)
33
+ return sourceFile
34
+ .getProject()
35
+ .getProgram()
36
+ .getSyntacticDiagnostics(sourceFile)
37
+ .map((diagnostic) => `${diagnostic.getLineNumber() ?? 0}: ${diagnostic.getMessageText()}`)
38
+ }
39
+
40
+ function registryObject(source: string): ObjectLiteralExpression {
41
+ const declaration = parse(source).getVariableDeclarationOrThrow('LUCIDE_ICON_REGISTRY')
42
+ return declaration.getInitializerIfKindOrThrow(SyntaxKind.ObjectLiteralExpression)
43
+ }
44
+
45
+ function registryEntries(source: string): Record<string, string> {
46
+ const entries: Record<string, string> = {}
47
+ for (const property of registryObject(source).getProperties()) {
48
+ const assignment = property.asKindOrThrow(SyntaxKind.PropertyAssignment)
49
+ entries[assignment.getSymbolOrThrow().getName()] = assignment.getInitializerOrThrow().getText()
50
+ }
51
+ return entries
52
+ }
53
+
54
+ function valueImportNames(source: string, moduleSpecifier: string): string[] {
55
+ return parse(source)
56
+ .getImportDeclarations()
57
+ .filter((declaration) => declaration.getModuleSpecifierValue() === moduleSpecifier)
58
+ .filter((declaration) => !declaration.isTypeOnly())
59
+ .flatMap((declaration) => declaration.getNamedImports().map((named) => named.getName()))
60
+ }
61
+
62
+ describe('buildLucideRegistrySource', () => {
63
+ it('maps every discovered kebab name to its lucide export', () => {
64
+ expect(registryEntries(buildLucideRegistrySource(populatedFixture))).toEqual({
65
+ 'alert-circle': 'AlertCircle',
66
+ 'bar-chart-2': 'BarChart2',
67
+ bell: 'Bell',
68
+ })
69
+ })
70
+
71
+ it('emits the registry keys sorted by kebab name', () => {
72
+ const keys = registryObject(buildLucideRegistrySource(populatedFixture))
73
+ .getProperties()
74
+ .map((property) => property.asKindOrThrow(SyntaxKind.PropertyAssignment).getSymbolOrThrow().getName())
75
+ expect(keys).toEqual(['alert-circle', 'bar-chart-2', 'bell'])
76
+ })
77
+
78
+ it('imports each lucide export exactly once', () => {
79
+ expect(valueImportNames(buildLucideRegistrySource(populatedFixture), 'lucide-react')).toEqual([
80
+ 'AlertCircle',
81
+ 'BarChart2',
82
+ 'Bell',
83
+ ])
84
+ })
85
+
86
+ it('quotes keys that are not valid identifiers', () => {
87
+ const property = registryObject(buildLucideRegistrySource(populatedFixture))
88
+ .getPropertyOrThrow("'bar-chart-2'")
89
+ .asKindOrThrow(SyntaxKind.PropertyAssignment)
90
+ expect(property.getNameNode().getKind()).toBe(SyntaxKind.StringLiteral)
91
+ })
92
+
93
+ it('de-duplicates the value import when two names share one export', () => {
94
+ const source = buildLucideRegistrySource(duplicateExportFixture)
95
+ expect(valueImportNames(source, 'lucide-react')).toEqual(['Trash'])
96
+ expect(registryEntries(source)).toEqual({ trash: 'Trash', 'trash-can': 'Trash' })
97
+ })
98
+
99
+ it('emits a valid module with an empty registry when no icons are discovered', () => {
100
+ const source = buildLucideRegistrySource([])
101
+ expect(registryObject(source).getProperties()).toHaveLength(0)
102
+ expect(valueImportNames(source, 'lucide-react')).toEqual([])
103
+ expect(source).not.toContain("import {} from 'lucide-react'")
104
+ })
105
+
106
+ it('keeps the registry a plain mutable object literal', () => {
107
+ const source = buildLucideRegistrySource(populatedFixture)
108
+ const declaration = parse(source).getVariableDeclarationOrThrow('LUCIDE_ICON_REGISTRY')
109
+ expect(declaration.getTypeNodeOrThrow().getText()).toBe('Record<string, LucideIcon>')
110
+ expect(declaration.getVariableStatementOrThrow().isExported()).toBe(true)
111
+ expect(source).not.toContain('as const')
112
+ expect(source).not.toContain('Object.freeze')
113
+ })
114
+
115
+ it('keeps the LucideIcon import type-only', () => {
116
+ const typeOnly = parse(buildLucideRegistrySource(populatedFixture))
117
+ .getImportDeclarations()
118
+ .filter((declaration) => declaration.isTypeOnly())
119
+ .flatMap((declaration) => declaration.getNamedImports().map((named) => named.getName()))
120
+ expect(typeOnly).toEqual(['LucideIcon'])
121
+ })
122
+
123
+ it.each([
124
+ ['populated', populatedFixture],
125
+ ['empty', [] as ResolvedIcon[]],
126
+ ['duplicate exports', duplicateExportFixture],
127
+ ])('emits %s output with zero syntactic diagnostics', (_label, fixture) => {
128
+ expect(syntacticDiagnostics(buildLucideRegistrySource(fixture))).toEqual([])
129
+ })
130
+
131
+ it('fails loudly when an export name would produce an unparseable module', () => {
132
+ expect(() => buildLucideRegistrySource([{ kebab: 'broken', exportName: 'not a valid export' }])).toThrow(
133
+ /not syntactically valid/
134
+ )
135
+ })
136
+ })
137
+
138
+ describe('lucideRegistry barrel', () => {
139
+ it('keeps its public export list unchanged', () => {
140
+ const project = new Project({ useInMemoryFileSystem: true })
141
+ const barrel = project.createSourceFile(
142
+ 'lucideRegistry.ts',
143
+ readFileSync(join(iconsDir, 'lucideRegistry.ts'), 'utf-8'),
144
+ { overwrite: true }
145
+ )
146
+ expect([...barrel.getExportedDeclarations().keys()].sort()).toEqual([
147
+ 'LUCIDE_ICON_REGISTRY',
148
+ 'registerAdditionalIcons',
149
+ 'resolveRegisteredLucideIcon',
150
+ 'resolveRegisteredLucideIconNode',
151
+ ])
152
+ })
153
+ })
154
+
155
+ describe('lucideRegistry.generated importers', () => {
156
+ it('is imported only from within src/backend/icons', () => {
157
+ const repoRoot = join(packageDir, '..', '..')
158
+ let matches: string[] = []
159
+ try {
160
+ matches = execFileSync(
161
+ 'git',
162
+ ['grep', '-l', 'lucideRegistry.generated', '--', '*.ts', '*.tsx'],
163
+ { cwd: repoRoot, encoding: 'utf-8' }
164
+ )
165
+ .split('\n')
166
+ .filter(Boolean)
167
+ } catch (error) {
168
+ const status = (error as { status?: number }).status
169
+ if (status !== 1) throw error
170
+ }
171
+
172
+ const outsideIconsFolder = matches.filter(
173
+ (path) => !path.startsWith('packages/ui/src/backend/icons/')
174
+ )
175
+ expect(outsideIconsFolder).toEqual([])
176
+ })
177
+ })
@@ -2,8 +2,7 @@
2
2
  // AUTO-GENERATED by @open-mercato/ui build. Do not edit by hand.
3
3
  // Generated from discovered icon string usages across the repo.
4
4
 
5
- import type * as React from 'react'
6
- import type { LucideIcon } from 'lucide-react'
5
+ import type { LucideIcon } from 'lucide-react';
7
6
  import {
8
7
  Activity,
9
8
  AlertCircle,
@@ -150,7 +149,7 @@ import {
150
149
  Wrench,
151
150
  X,
152
151
  XCircle,
153
- } from 'lucide-react'
152
+ } from 'lucide-react';
154
153
 
155
154
  export const LUCIDE_ICON_REGISTRY: Record<string, LucideIcon> = {
156
155
  'activity': Activity,
@@ -298,37 +297,4 @@ export const LUCIDE_ICON_REGISTRY: Record<string, LucideIcon> = {
298
297
  'wrench': Wrench,
299
298
  'x': X,
300
299
  'x-circle': XCircle,
301
- }
302
-
303
- function normalizeKebabIconName(input: string): string {
304
- const trimmed = input.trim()
305
- if (!trimmed) return ''
306
- const withoutPrefix = trimmed.startsWith('lucide:') ? trimmed.slice('lucide:'.length) : trimmed
307
- if (!withoutPrefix) return ''
308
- if (!withoutPrefix.includes('-') && !withoutPrefix.includes('_') && !withoutPrefix.includes(' ') && /[A-Z]/.test(withoutPrefix)) {
309
- return withoutPrefix
310
- .replace(/([a-z0-9])([A-Z])/g, '$1-$2')
311
- .replace(/([A-Z])([A-Z][a-z])/g, '$1-$2')
312
- .toLowerCase()
313
- }
314
- return withoutPrefix
315
- .replace(/[_\s]+/g, '-')
316
- .replace(/-+/g, '-')
317
- .toLowerCase()
318
- }
319
-
320
- export function resolveRegisteredLucideIcon(name: string | undefined): LucideIcon | null {
321
- if (!name) return null
322
- const normalized = normalizeKebabIconName(name)
323
- if (!normalized) return null
324
- return (LUCIDE_ICON_REGISTRY as Record<string, LucideIcon | undefined>)[normalized] ?? null
325
- }
326
-
327
- export function resolveRegisteredLucideIconNode(
328
- name: string | undefined,
329
- className: string
330
- ): React.ReactNode | null {
331
- const Icon = resolveRegisteredLucideIcon(name)
332
- if (!Icon) return null
333
- return <Icon className={className} />
334
- }
300
+ };
@@ -1,8 +1,8 @@
1
1
  export {
2
2
  resolveRegisteredLucideIcon,
3
3
  resolveRegisteredLucideIconNode,
4
- LUCIDE_ICON_REGISTRY,
5
- } from './lucideRegistry.generated'
4
+ } from './lucideRegistryRuntime'
5
+ export { LUCIDE_ICON_REGISTRY } from './lucideRegistry.generated'
6
6
 
7
7
  import type { LucideIcon } from 'lucide-react'
8
8
  import { LUCIDE_ICON_REGISTRY } from './lucideRegistry.generated'
@@ -0,0 +1,36 @@
1
+ import type * as React from 'react'
2
+ import type { LucideIcon } from 'lucide-react'
3
+ import { LUCIDE_ICON_REGISTRY } from './lucideRegistry.generated'
4
+
5
+ function normalizeKebabIconName(input: string): string {
6
+ const trimmed = input.trim()
7
+ if (!trimmed) return ''
8
+ const withoutPrefix = trimmed.startsWith('lucide:') ? trimmed.slice('lucide:'.length) : trimmed
9
+ if (!withoutPrefix) return ''
10
+ if (!withoutPrefix.includes('-') && !withoutPrefix.includes('_') && !withoutPrefix.includes(' ') && /[A-Z]/.test(withoutPrefix)) {
11
+ return withoutPrefix
12
+ .replace(/([a-z0-9])([A-Z])/g, '$1-$2')
13
+ .replace(/([A-Z])([A-Z][a-z])/g, '$1-$2')
14
+ .toLowerCase()
15
+ }
16
+ return withoutPrefix
17
+ .replace(/[_\s]+/g, '-')
18
+ .replace(/-+/g, '-')
19
+ .toLowerCase()
20
+ }
21
+
22
+ export function resolveRegisteredLucideIcon(name: string | undefined): LucideIcon | null {
23
+ if (!name) return null
24
+ const normalized = normalizeKebabIconName(name)
25
+ if (!normalized) return null
26
+ return (LUCIDE_ICON_REGISTRY as Record<string, LucideIcon | undefined>)[normalized] ?? null
27
+ }
28
+
29
+ export function resolveRegisteredLucideIconNode(
30
+ name: string | undefined,
31
+ className: string
32
+ ): React.ReactNode | null {
33
+ const Icon = resolveRegisteredLucideIcon(name)
34
+ if (!Icon) return null
35
+ return <Icon className={className} />
36
+ }