@open-mercato/ui 0.7.1-develop.7131.1.20abf9e885 → 0.7.1-develop.7135.1.19bf969756

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/build.mjs CHANGED
@@ -1,93 +1,15 @@
1
- import { glob } from 'glob'
2
- import { readFileSync, writeFileSync } from 'node:fs'
1
+ import { writeFileSync } from 'node:fs'
3
2
  import { dirname, join } from 'node:path'
4
3
  import { fileURLToPath } from 'node:url'
5
4
  import { buildPackage } from '../../scripts/build-package.mjs'
5
+ import { discoverResolvedIcons } from './scripts/lucideIconDiscovery.cjs'
6
6
  import { buildLucideRegistrySource } from './scripts/lucideRegistrySource.cjs'
7
7
 
8
8
  const packageDir = dirname(fileURLToPath(import.meta.url))
9
9
 
10
- function normalizeKebabIconName(input) {
11
- const trimmed = input.trim()
12
- if (!trimmed) return ''
13
- const withoutPrefix = trimmed.startsWith('lucide:') ? trimmed.slice('lucide:'.length) : trimmed
14
- if (!withoutPrefix) return ''
15
- if (!withoutPrefix.includes('-') && !withoutPrefix.includes('_') && !withoutPrefix.includes(' ') && /[A-Z]/.test(withoutPrefix)) {
16
- return withoutPrefix
17
- .replace(/([a-z0-9])([A-Z])/g, '$1-$2')
18
- .replace(/([A-Z])([A-Z][a-z])/g, '$1-$2')
19
- .toLowerCase()
20
- }
21
- return withoutPrefix
22
- .replace(/[_\s]+/g, '-')
23
- .replace(/-+/g, '-')
24
- .toLowerCase()
25
- }
26
-
27
- function kebabToLucideExportName(kebab) {
28
- return kebab
29
- .split('-')
30
- .filter(Boolean)
31
- .map((part) => {
32
- if (/^\d+$/.test(part)) return part
33
- return part.charAt(0).toUpperCase() + part.slice(1)
34
- })
35
- .join('')
36
- }
37
-
38
10
  async function generateLucideRegistry() {
39
11
  const repoRoot = join(packageDir, '..', '..')
40
- const candidateFiles = await glob(['apps/**/*.{ts,tsx}', 'packages/**/*.{ts,tsx}'], {
41
- cwd: repoRoot,
42
- absolute: true,
43
- ignore: [
44
- '**/node_modules/**',
45
- '**/dist/**',
46
- '**/.mercato/**',
47
- 'packages/core/generated/**',
48
- '**/__tests__/**',
49
- '**/*.test.ts',
50
- '**/*.test.tsx',
51
- '**/*.generated.ts',
52
- '**/*.generated.tsx',
53
- '**/*.d.ts',
54
- ],
55
- })
56
-
57
- const iconStrings = new Set()
58
- const patterns = [
59
- /\bicon\s*:\s*['"`]([^'"`]+)['"`]/g,
60
- /\bicon\s*=\s*['"`]([^'"`]+)['"`]/g,
61
- ]
62
-
63
- for (const absPath of candidateFiles) {
64
- const content = readFileSync(absPath, 'utf-8')
65
- for (const pattern of patterns) {
66
- pattern.lastIndex = 0
67
- for (;;) {
68
- const match = pattern.exec(content)
69
- if (!match) break
70
- const raw = match[1]
71
- if (!raw) continue
72
- const normalized = normalizeKebabIconName(raw)
73
- if (!normalized) continue
74
- iconStrings.add(normalized)
75
- }
76
- }
77
- }
78
-
79
- const lucide = await import('lucide-react')
80
- const exportKeys = new Set(Object.keys(lucide))
81
- const excludedLucideExports = new Set(['Icon'])
82
-
83
- const resolved = []
84
- for (const name of iconStrings) {
85
- const exportName = kebabToLucideExportName(name)
86
- if (excludedLucideExports.has(exportName)) continue
87
- if (exportKeys.has(exportName)) {
88
- resolved.push({ kebab: name, exportName })
89
- }
90
- }
12
+ const resolved = await discoverResolvedIcons(repoRoot)
91
13
 
92
14
  const outPath = join(packageDir, 'src/backend/icons/lucideRegistry.generated.tsx')
93
15
  writeFileSync(outPath, buildLucideRegistrySource(resolved))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-mercato/ui",
3
- "version": "0.7.1-develop.7131.1.20abf9e885",
3
+ "version": "0.7.1-develop.7135.1.19bf969756",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -155,20 +155,21 @@
155
155
  "remark-gfm": "^4.0.1"
156
156
  },
157
157
  "peerDependencies": {
158
- "@open-mercato/shared": "0.7.1-develop.7131.1.20abf9e885",
158
+ "@open-mercato/shared": "0.7.1-develop.7135.1.19bf969756",
159
159
  "react": ">=18.0.0",
160
160
  "react-dom": ">=18.0.0",
161
161
  "react-is": ">=18.0.0"
162
162
  },
163
163
  "devDependencies": {
164
164
  "@figma/code-connect": "^1.3.4",
165
- "@open-mercato/shared": "0.7.1-develop.7131.1.20abf9e885",
165
+ "@open-mercato/shared": "0.7.1-develop.7135.1.19bf969756",
166
166
  "@testing-library/dom": "^10.4.1",
167
167
  "@testing-library/jest-dom": "^7.0.0",
168
168
  "@testing-library/react": "^16.3.1",
169
169
  "@types/jest": "^30.0.0",
170
170
  "@types/react": "^19.2.17",
171
171
  "@types/react-dom": "^19.2.3",
172
+ "glob": "^13.0.6",
172
173
  "jest": "^30.4.2",
173
174
  "jest-environment-jsdom": "^30.4.1",
174
175
  "react": "19.2.8",
@@ -0,0 +1,108 @@
1
+ // Pure discovery pass feeding src/backend/icons/lucideRegistry.generated.tsx.
2
+ // Kept side-effect free, and CommonJS so both build.mjs and the Jest suite can load it.
3
+
4
+ const { glob } = require('glob')
5
+ const { readFileSync } = require('node:fs')
6
+
7
+ const SOURCE_GLOBS = ['apps/**/*.{ts,tsx}', 'packages/**/*.{ts,tsx}']
8
+
9
+ const IGNORED_GLOBS = [
10
+ '**/node_modules/**',
11
+ '**/dist/**',
12
+ '**/.mercato/**',
13
+ 'packages/core/generated/**',
14
+ '**/__tests__/**',
15
+ '**/*.test.ts',
16
+ '**/*.test.tsx',
17
+ '**/*.generated.ts',
18
+ '**/*.generated.tsx',
19
+ '**/*.d.ts',
20
+ ]
21
+
22
+ const ICON_STRING_PATTERNS = [
23
+ /\bicon\s*:\s*['"`]([^'"`]+)['"`]/g,
24
+ /\bicon\s*=\s*['"`]([^'"`]+)['"`]/g,
25
+ ]
26
+
27
+ const EXCLUDED_LUCIDE_EXPORTS = new Set(['Icon'])
28
+
29
+ function normalizeKebabIconName(input) {
30
+ const trimmed = input.trim()
31
+ if (!trimmed) return ''
32
+ const withoutPrefix = trimmed.startsWith('lucide:') ? trimmed.slice('lucide:'.length) : trimmed
33
+ if (!withoutPrefix) return ''
34
+ if (!withoutPrefix.includes('-') && !withoutPrefix.includes('_') && !withoutPrefix.includes(' ') && /[A-Z]/.test(withoutPrefix)) {
35
+ return withoutPrefix
36
+ .replace(/([a-z0-9])([A-Z])/g, '$1-$2')
37
+ .replace(/([A-Z])([A-Z][a-z])/g, '$1-$2')
38
+ .toLowerCase()
39
+ }
40
+ return withoutPrefix
41
+ .replace(/[_\s]+/g, '-')
42
+ .replace(/-+/g, '-')
43
+ .toLowerCase()
44
+ }
45
+
46
+ function kebabToLucideExportName(kebab) {
47
+ return kebab
48
+ .split('-')
49
+ .filter(Boolean)
50
+ .map((part) => {
51
+ if (/^\d+$/.test(part)) return part
52
+ return part.charAt(0).toUpperCase() + part.slice(1)
53
+ })
54
+ .join('')
55
+ }
56
+
57
+ async function discoverIconNames(repoRoot) {
58
+ const candidateFiles = await glob(SOURCE_GLOBS, {
59
+ cwd: repoRoot,
60
+ absolute: true,
61
+ ignore: IGNORED_GLOBS,
62
+ })
63
+
64
+ const iconStrings = new Set()
65
+ for (const absPath of candidateFiles) {
66
+ const content = readFileSync(absPath, 'utf-8')
67
+ for (const pattern of ICON_STRING_PATTERNS) {
68
+ pattern.lastIndex = 0
69
+ for (;;) {
70
+ const match = pattern.exec(content)
71
+ if (!match) break
72
+ const raw = match[1]
73
+ if (!raw) continue
74
+ const normalized = normalizeKebabIconName(raw)
75
+ if (!normalized) continue
76
+ iconStrings.add(normalized)
77
+ }
78
+ }
79
+ }
80
+
81
+ return iconStrings
82
+ }
83
+
84
+ function resolveIconNames(iconNames, exportNames) {
85
+ const exportKeys = exportNames instanceof Set ? exportNames : new Set(exportNames)
86
+ const resolved = []
87
+ for (const kebab of iconNames) {
88
+ const exportName = kebabToLucideExportName(kebab)
89
+ if (EXCLUDED_LUCIDE_EXPORTS.has(exportName)) continue
90
+ if (exportKeys.has(exportName)) {
91
+ resolved.push({ kebab, exportName })
92
+ }
93
+ }
94
+ return resolved
95
+ }
96
+
97
+ async function discoverResolvedIcons(repoRoot) {
98
+ const lucide = await import('lucide-react')
99
+ return resolveIconNames(await discoverIconNames(repoRoot), Object.keys(lucide))
100
+ }
101
+
102
+ module.exports = {
103
+ normalizeKebabIconName,
104
+ kebabToLucideExportName,
105
+ discoverIconNames,
106
+ resolveIconNames,
107
+ discoverResolvedIcons,
108
+ }
@@ -1,8 +1,9 @@
1
1
  import { execFileSync } from 'node:child_process'
2
- import { readFileSync } from 'node:fs'
2
+ import { existsSync, readFileSync } from 'node:fs'
3
3
  import { tmpdir } from 'node:os'
4
4
  import { join } from 'node:path'
5
5
  import { Project, ScriptKind, SyntaxKind, type ObjectLiteralExpression, type SourceFile } from 'ts-morph'
6
+ import { discoverResolvedIcons } from '../../../../scripts/lucideIconDiscovery.cjs'
6
7
  import { buildLucideRegistrySource } from '../../../../scripts/lucideRegistrySource.cjs'
7
8
  import jestConfig from '../../../../jest.config.cjs'
8
9
 
@@ -225,6 +226,52 @@ describe('lucideRegistry.generated importers', () => {
225
226
  })
226
227
  })
227
228
 
229
+ type MonorepoProbe = { available: true } | { available: false; reason: string }
230
+
231
+ function detectMonorepoCheckout(repoRoot: string): MonorepoProbe {
232
+ const generatorPath = join(repoRoot, 'packages', 'ui', 'build.mjs')
233
+ if (!existsSync(generatorPath)) return { available: false, reason: `${generatorPath} is missing` }
234
+ return { available: true }
235
+ }
236
+
237
+ // The registry is build output, so a module can reference an icon string and merge
238
+ // while the committed file still lacks it — the icon then silently renders as nothing.
239
+ // Running the real discovery pass here turns that drift into a failing test.
240
+ describe('committed lucideRegistry.generated.tsx', () => {
241
+ const repoRoot = join(packageDir, '..', '..')
242
+ const committedSource = readFileSync(join(iconsDir, 'lucideRegistry.generated.tsx'), 'utf-8')
243
+ const regenerationHint = 'run `yarn build:packages` and commit the regenerated file'
244
+ // The scan reads the repo, so — like the `git grep` guard above — it can only run
245
+ // inside the monorepo. Published tarballs ship these tests without the sources they
246
+ // scan, where an empty scan would fail an invariant it cannot evaluate rather than
247
+ // report real drift. There, this degrades to a skip that states why.
248
+ const checkout = detectMonorepoCheckout(repoRoot)
249
+ const itInsideMonorepo = checkout.available ? it : it.skip
250
+ const skipReason = checkout.available ? '' : ` — skipped: ${checkout.reason}`
251
+ let discoveredIcons: ResolvedIcon[] = []
252
+
253
+ beforeAll(async () => {
254
+ if (!checkout.available) return
255
+ discoveredIcons = await discoverResolvedIcons(repoRoot)
256
+ })
257
+
258
+ itInsideMonorepo(`registers every icon string used across the repo — ${regenerationHint}${skipReason}`, () => {
259
+ const registered = new Set(Object.keys(registryEntries(committedSource)))
260
+ const missing = discoveredIcons.map((icon) => icon.kebab).filter((kebab) => !registered.has(kebab))
261
+ expect(missing).toEqual([])
262
+ })
263
+
264
+ itInsideMonorepo(`is byte-identical to a fresh generator run — ${regenerationHint}${skipReason}`, () => {
265
+ expect(committedSource).toBe(buildLucideRegistrySource(discoveredIcons))
266
+ })
267
+
268
+ it('states a reason instead of asserting when the monorepo sources are absent', () => {
269
+ const probe = detectMonorepoCheckout(join(tmpdir(), 'lucide-registry-absent-monorepo'))
270
+ expect(probe.available).toBe(false)
271
+ expect(probe.available ? '' : probe.reason).toContain('build.mjs')
272
+ })
273
+ })
274
+
228
275
  describe('jest transform scope', () => {
229
276
  const transformPatterns = Object.keys(jestConfig.transform).map((key) => new RegExp(key))
230
277