@pro-laico/payload-icons 0.0.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.
Files changed (119) hide show
  1. package/LICENSE.md +22 -0
  2. package/README.md +9 -0
  3. package/dist/cache/index.d.ts +9 -0
  4. package/dist/cache/index.d.ts.map +1 -0
  5. package/dist/cache/index.js +96 -0
  6. package/dist/cache/index.js.map +1 -0
  7. package/dist/collections/Icon.d.ts +14 -0
  8. package/dist/collections/Icon.d.ts.map +1 -0
  9. package/dist/collections/Icon.js +96 -0
  10. package/dist/collections/Icon.js.map +1 -0
  11. package/dist/collections/IconRequest.d.ts +30 -0
  12. package/dist/collections/IconRequest.d.ts.map +1 -0
  13. package/dist/collections/IconRequest.js +82 -0
  14. package/dist/collections/IconRequest.js.map +1 -0
  15. package/dist/collections/IconSet.d.ts +43 -0
  16. package/dist/collections/IconSet.d.ts.map +1 -0
  17. package/dist/collections/IconSet.js +168 -0
  18. package/dist/collections/IconSet.js.map +1 -0
  19. package/dist/components/Icon.d.ts +35 -0
  20. package/dist/components/Icon.d.ts.map +1 -0
  21. package/dist/components/Icon.js +56 -0
  22. package/dist/components/Icon.js.map +1 -0
  23. package/dist/components/admin/clearIconRequests.d.ts +14 -0
  24. package/dist/components/admin/clearIconRequests.d.ts.map +1 -0
  25. package/dist/components/admin/clearIconRequests.js +52 -0
  26. package/dist/components/admin/clearIconRequests.js.map +1 -0
  27. package/dist/components/admin/iconPreview.d.ts +7 -0
  28. package/dist/components/admin/iconPreview.d.ts.map +1 -0
  29. package/dist/components/admin/iconPreview.js +49 -0
  30. package/dist/components/admin/iconPreview.js.map +1 -0
  31. package/dist/components/admin/iconRowLabel.d.ts +8 -0
  32. package/dist/components/admin/iconRowLabel.d.ts.map +1 -0
  33. package/dist/components/admin/iconRowLabel.js +28 -0
  34. package/dist/components/admin/iconRowLabel.js.map +1 -0
  35. package/dist/components/admin/iconUsagePanel.client.d.ts +26 -0
  36. package/dist/components/admin/iconUsagePanel.client.d.ts.map +1 -0
  37. package/dist/components/admin/iconUsagePanel.client.js +290 -0
  38. package/dist/components/admin/iconUsagePanel.client.js.map +1 -0
  39. package/dist/components/admin/iconUsagePanel.d.ts +12 -0
  40. package/dist/components/admin/iconUsagePanel.d.ts.map +1 -0
  41. package/dist/components/admin/iconUsagePanel.js +68 -0
  42. package/dist/components/admin/iconUsagePanel.js.map +1 -0
  43. package/dist/hooks/formatSVG.d.ts +22 -0
  44. package/dist/hooks/formatSVG.d.ts.map +1 -0
  45. package/dist/hooks/formatSVG.js +270 -0
  46. package/dist/hooks/formatSVG.js.map +1 -0
  47. package/dist/index.d.ts +7 -0
  48. package/dist/index.d.ts.map +1 -0
  49. package/dist/index.js +10 -0
  50. package/dist/index.js.map +1 -0
  51. package/dist/lib/activeField.d.ts +24 -0
  52. package/dist/lib/activeField.d.ts.map +1 -0
  53. package/dist/lib/activeField.js +71 -0
  54. package/dist/lib/activeField.js.map +1 -0
  55. package/dist/lib/authenticated.d.ts +5 -0
  56. package/dist/lib/authenticated.d.ts.map +1 -0
  57. package/dist/lib/authenticated.js +4 -0
  58. package/dist/lib/authenticated.js.map +1 -0
  59. package/dist/lib/defaultAccess.d.ts +8 -0
  60. package/dist/lib/defaultAccess.d.ts.map +1 -0
  61. package/dist/lib/defaultAccess.js +6 -0
  62. package/dist/lib/defaultAccess.js.map +1 -0
  63. package/dist/lib/extractSVG.d.ts +3 -0
  64. package/dist/lib/extractSVG.d.ts.map +1 -0
  65. package/dist/lib/extractSVG.js +27 -0
  66. package/dist/lib/extractSVG.js.map +1 -0
  67. package/dist/lib/getPayloadClient.d.ts +22 -0
  68. package/dist/lib/getPayloadClient.d.ts.map +1 -0
  69. package/dist/lib/getPayloadClient.js +39 -0
  70. package/dist/lib/getPayloadClient.js.map +1 -0
  71. package/dist/lib/mergeHooks.d.ts +8 -0
  72. package/dist/lib/mergeHooks.d.ts.map +1 -0
  73. package/dist/lib/mergeHooks.js +19 -0
  74. package/dist/lib/mergeHooks.js.map +1 -0
  75. package/dist/lib/titleCase.d.ts +7 -0
  76. package/dist/lib/titleCase.d.ts.map +1 -0
  77. package/dist/lib/titleCase.js +17 -0
  78. package/dist/lib/titleCase.js.map +1 -0
  79. package/dist/plugin.d.ts +91 -0
  80. package/dist/plugin.d.ts.map +1 -0
  81. package/dist/plugin.js +72 -0
  82. package/dist/plugin.js.map +1 -0
  83. package/dist/scan/cli.d.ts +14 -0
  84. package/dist/scan/cli.d.ts.map +1 -0
  85. package/dist/scan/cli.js +114 -0
  86. package/dist/scan/cli.js.map +1 -0
  87. package/dist/scan/extract.d.ts +58 -0
  88. package/dist/scan/extract.d.ts.map +1 -0
  89. package/dist/scan/extract.js +286 -0
  90. package/dist/scan/extract.js.map +1 -0
  91. package/dist/scan/index.d.ts +81 -0
  92. package/dist/scan/index.d.ts.map +1 -0
  93. package/dist/scan/index.js +133 -0
  94. package/dist/scan/index.js.map +1 -0
  95. package/dist/scan/live.d.ts +20 -0
  96. package/dist/scan/live.d.ts.map +1 -0
  97. package/dist/scan/live.js +109 -0
  98. package/dist/scan/live.js.map +1 -0
  99. package/dist/scan/load.d.ts +28 -0
  100. package/dist/scan/load.d.ts.map +1 -0
  101. package/dist/scan/load.js +37 -0
  102. package/dist/scan/load.js.map +1 -0
  103. package/dist/scan/types.d.ts +29 -0
  104. package/dist/scan/types.d.ts.map +1 -0
  105. package/dist/scan/types.js +8 -0
  106. package/dist/scan/types.js.map +1 -0
  107. package/dist/types.d.ts +38 -0
  108. package/dist/types.d.ts.map +1 -0
  109. package/dist/types.js +3 -0
  110. package/dist/types.js.map +1 -0
  111. package/dist/usage/recordMiss.d.ts +17 -0
  112. package/dist/usage/recordMiss.d.ts.map +1 -0
  113. package/dist/usage/recordMiss.js +60 -0
  114. package/dist/usage/recordMiss.js.map +1 -0
  115. package/dist/usage/trackIconMiss.d.ts +16 -0
  116. package/dist/usage/trackIconMiss.d.ts.map +1 -0
  117. package/dist/usage/trackIconMiss.js +43 -0
  118. package/dist/usage/trackIconMiss.js.map +1 -0
  119. package/package.json +106 -0
@@ -0,0 +1,286 @@
1
+ /**
2
+ * Pure, dependency-free extraction of literal icon names from one source file's
3
+ * text. This is the core of the build-time usage scan that powers the admin
4
+ * "requested icons" panel — kept side-effect-free (no `fs`) so it is trivially
5
+ * unit-testable and reusable from any walker.
6
+ *
7
+ * It is tag-aware, NOT a naive regex. The scanner walks the file skipping
8
+ * strings and comments, and for every `<Icon …>` opening tag it parses the
9
+ * attribute list, so a `>` inside an expression (`check={a > b}`) or a `name`
10
+ * substring inside a sibling attribute (`iconName`, `data-name`) never trips
11
+ * it.
12
+ *
13
+ * ONLY string-literal names are collected — the value must resolve statically:
14
+ * - `name="x"` / `name='x'` (direct attribute strings)
15
+ * - `name={"x"}` / `name={'x'}` (string literal in an expression slot)
16
+ * - `` name={`x`} `` (template literal, no interpolation)
17
+ * Dynamic `name={expr}` (variables, concatenation, interpolated templates) is
18
+ * intentionally skipped — a static scan cannot know its value.
19
+ */ /** A single literal `<Icon name="…">` occurrence found in a file. */ const DEFAULT_COMPONENTS = [
20
+ 'Icon'
21
+ ];
22
+ /** Plausible icon names only — set-row names are kebab-cased, so prose placeholders (`…`) never qualify. */ const PLAUSIBLE_NAME = /^[\w-]+$/;
23
+ const isWhitespace = (c)=>c === ' ' || c === '\t' || c === '\n' || c === '\r' || c === '\f' || c === '\v';
24
+ const isIdentStart = (c)=>/[A-Za-z_$]/.test(c);
25
+ const isTagNameBoundary = (c)=>c === undefined || isWhitespace(c) || c === '/' || c === '>';
26
+ /** Builds a binary-searchable index of line-start offsets for fast line/column lookup. */ const buildLineIndex = (source)=>{
27
+ const starts = [
28
+ 0
29
+ ];
30
+ for(let i = 0; i < source.length; i++)if (source[i] === '\n') starts.push(i + 1);
31
+ return starts;
32
+ };
33
+ const lineColAt = (lineStarts, index)=>{
34
+ // Binary search for the greatest line-start <= index.
35
+ let lo = 0;
36
+ let hi = lineStarts.length - 1;
37
+ while(lo < hi){
38
+ const mid = lo + hi + 1 >> 1;
39
+ if (lineStarts[mid] <= index) lo = mid;
40
+ else hi = mid - 1;
41
+ }
42
+ return {
43
+ line: lo + 1,
44
+ column: index - lineStarts[lo] + 1
45
+ };
46
+ };
47
+ /** Skips a `"…"` / `'…'` string starting at `i` (the opening quote). Returns the index just past the closing quote. */ const skipString = (source, i)=>{
48
+ const quote = source[i];
49
+ i++;
50
+ while(i < source.length){
51
+ const c = source[i];
52
+ if (c === '\\') {
53
+ i += 2;
54
+ continue;
55
+ }
56
+ if (c === quote) return i + 1;
57
+ i++;
58
+ }
59
+ return i;
60
+ };
61
+ /** Skips a `` `…` `` template (including nested `${ … }`) starting at the opening backtick. Returns the index just past the closing backtick. */ const skipTemplate = (source, i)=>{
62
+ i++;
63
+ while(i < source.length){
64
+ const c = source[i];
65
+ if (c === '\\') {
66
+ i += 2;
67
+ continue;
68
+ }
69
+ if (c === '`') return i + 1;
70
+ if (c === '$' && source[i + 1] === '{') {
71
+ i = skipBraces(source, i + 1);
72
+ continue;
73
+ }
74
+ i++;
75
+ }
76
+ return i;
77
+ };
78
+ /** Skips a balanced `{ … }` region starting at the opening brace, honoring nested braces, strings, templates, and comments. Returns the index just past the closing brace. */ const skipBraces = (source, i)=>{
79
+ let depth = 0;
80
+ while(i < source.length){
81
+ const c = source[i];
82
+ if (c === '"' || c === "'") {
83
+ i = skipString(source, i);
84
+ continue;
85
+ }
86
+ if (c === '`') {
87
+ i = skipTemplate(source, i);
88
+ continue;
89
+ }
90
+ if (c === '/' && source[i + 1] === '/') {
91
+ i = skipLineComment(source, i);
92
+ continue;
93
+ }
94
+ if (c === '/' && source[i + 1] === '*') {
95
+ i = skipBlockComment(source, i);
96
+ continue;
97
+ }
98
+ if (c === '{') depth++;
99
+ else if (c === '}') {
100
+ depth--;
101
+ if (depth === 0) return i + 1;
102
+ }
103
+ i++;
104
+ }
105
+ return i;
106
+ };
107
+ const skipLineComment = (source, i)=>{
108
+ i += 2;
109
+ while(i < source.length && source[i] !== '\n')i++;
110
+ return i;
111
+ };
112
+ const skipBlockComment = (source, i)=>{
113
+ i += 2;
114
+ while(i < source.length && !(source[i] === '*' && source[i + 1] === '/'))i++;
115
+ return Math.min(i + 2, source.length);
116
+ };
117
+ /**
118
+ * Interprets a JSX attribute expression container's INNER text (the part
119
+ * between `{` and `}`). Returns the literal string if the whole expression is a
120
+ * single string or non-interpolated template literal, otherwise `null`
121
+ * (dynamic — skipped).
122
+ */ const interpretExpression = (inner)=>{
123
+ const expr = inner.trim();
124
+ if (expr.length < 2) return null;
125
+ const first = expr[0];
126
+ const last = expr[expr.length - 1];
127
+ if ((first === '"' || first === "'") && last === first) {
128
+ // Must be a SINGLE string literal — reject concatenation like `"a" + b`.
129
+ if (skipString(expr, 0) !== expr.length) return null;
130
+ return unquote(expr);
131
+ }
132
+ if (first === '`' && last === '`') {
133
+ if (skipTemplate(expr, 0) !== expr.length) return null;
134
+ if (expr.includes('${')) return null // interpolated — dynamic
135
+ ;
136
+ return expr.slice(1, -1);
137
+ }
138
+ return null;
139
+ };
140
+ /** Decodes a quoted string literal's escapes into its runtime value. */ const unquote = (literal)=>{
141
+ const body = literal.slice(1, -1);
142
+ return body.replace(/\\(.)/g, (_, ch)=>{
143
+ switch(ch){
144
+ case 'n':
145
+ return '\n';
146
+ case 't':
147
+ return '\t';
148
+ case 'r':
149
+ return '\r';
150
+ default:
151
+ return ch;
152
+ }
153
+ });
154
+ };
155
+ /**
156
+ * Parses the attribute list of an `<Icon …>` opening tag and, if a literal
157
+ * `name` is present, returns it with the absolute source index of its value.
158
+ *
159
+ * @param source full file text
160
+ * @param start index of the first attribute char (just past the tag name)
161
+ * @returns the literal name + value index, or `null` if absent/dynamic
162
+ */ const parseNameAttr = (source, start)=>{
163
+ let i = start;
164
+ while(i < source.length){
165
+ const c = source.charAt(i);
166
+ if (isWhitespace(c)) {
167
+ i++;
168
+ continue;
169
+ }
170
+ if (c === '>') return null // end of opening tag, no name found
171
+ ;
172
+ if (c === '/' && source[i + 1] === '>') return null // self-closing, no name
173
+ ;
174
+ if (c === '{') {
175
+ // Spread attribute `{...props}` — skip it wholesale.
176
+ i = skipBraces(source, i);
177
+ continue;
178
+ }
179
+ if (!isIdentStart(c) && c !== '-') {
180
+ i++;
181
+ continue;
182
+ }
183
+ // Read an attribute name.
184
+ const attrStart = i;
185
+ while(i < source.length && /[\w:-]/.test(source.charAt(i)))i++;
186
+ const attrName = source.slice(attrStart, i);
187
+ // Skip whitespace before a possible `=`.
188
+ let j = i;
189
+ while(j < source.length && isWhitespace(source.charAt(j)))j++;
190
+ if (source[j] !== '=') {
191
+ // Boolean attribute (no value) — continue scanning from here.
192
+ i = j;
193
+ continue;
194
+ }
195
+ j++; // past '='
196
+ while(j < source.length && isWhitespace(source.charAt(j)))j++;
197
+ const valChar = source[j];
198
+ let valueIndex = j;
199
+ let value;
200
+ if (valChar === '"' || valChar === "'") {
201
+ const end = skipString(source, j);
202
+ value = unquote(source.slice(j, end));
203
+ i = end;
204
+ } else if (valChar === '{') {
205
+ const end = skipBraces(source, j);
206
+ value = interpretExpression(source.slice(j + 1, end - 1));
207
+ valueIndex = j + 1;
208
+ i = end;
209
+ } else {
210
+ // Malformed/unexpected — bail on this tag.
211
+ return null;
212
+ }
213
+ if (attrName === 'name') {
214
+ return value === null ? null : {
215
+ name: value,
216
+ index: valueIndex
217
+ };
218
+ }
219
+ }
220
+ return null;
221
+ };
222
+ /**
223
+ * Extracts every literal icon-name usage from a single file's source text.
224
+ * See the module doc for what counts as a literal. Returns occurrences in
225
+ * source order; de-duplication across files happens in the aggregator.
226
+ *
227
+ * The scan looks directly for `<Icon …>` tag starts and parses each opening tag
228
+ * with full string/brace awareness (so the `name` value and the tag's end are
229
+ * found correctly). It deliberately does NOT try to mask surrounding strings or
230
+ * comments: JSX text routinely contains apostrophes (`it's`) and `//` (`http://`)
231
+ * that a whole-file lexer would mis-read as a string/comment and skip a real
232
+ * usage past. Instead, two surgical guards keep commentary out without a lexer:
233
+ * a candidate whose line leads with `//` or `*` (line comments, JSDoc
234
+ * continuations — real JSX never starts a line that way) is skipped, and
235
+ * extracted names must look like icon names ({@link PLAUSIBLE_NAME}), so prose
236
+ * placeholders like `…` are dropped. An `<Icon name="…">` written literally in
237
+ * a JS string (or a mid-line comment) can still be picked up — a harmless
238
+ * over-count for an inventory, where missing a real usage would be the worse
239
+ * failure.
240
+ */ export const extractIconUsages = (source, options = {})=>{
241
+ const components = options.components?.length ? options.components : DEFAULT_COMPONENTS;
242
+ const componentSet = new Set(components);
243
+ const lineStarts = buildLineIndex(source);
244
+ const usages = [];
245
+ const n = source.length;
246
+ let i = 0;
247
+ while(i < n){
248
+ if (source[i] !== '<') {
249
+ i++;
250
+ continue;
251
+ }
252
+ // Candidate opening tag — read the tag name and check it against the set.
253
+ let k = i + 1;
254
+ if (k >= n || !isIdentStart(source.charAt(k))) {
255
+ i++;
256
+ continue;
257
+ }
258
+ const nameStart = k;
259
+ while(k < n && /[\w.]/.test(source.charAt(k)))k++;
260
+ const tagName = source.slice(nameStart, k);
261
+ if (!componentSet.has(tagName) || !isTagNameBoundary(source[k])) {
262
+ i = k;
263
+ continue;
264
+ }
265
+ // Comment guard: only the text between the line start and this `<` counts,
266
+ // so a URL's `//` earlier on the line never triggers a false skip.
267
+ const linePrefix = source.slice(source.lastIndexOf('\n', i - 1) + 1, i).trimStart();
268
+ if (linePrefix.startsWith('//') || linePrefix.startsWith('*')) {
269
+ i = k;
270
+ continue;
271
+ }
272
+ const found = parseNameAttr(source, k);
273
+ if (found && PLAUSIBLE_NAME.test(found.name.trim())) {
274
+ const { line, column } = lineColAt(lineStarts, found.index);
275
+ usages.push({
276
+ name: found.name.trim(),
277
+ line,
278
+ column
279
+ });
280
+ }
281
+ i = k;
282
+ }
283
+ return usages;
284
+ };
285
+
286
+ //# sourceMappingURL=extract.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/scan/extract.ts"],"sourcesContent":["/**\n * Pure, dependency-free extraction of literal icon names from one source file's\n * text. This is the core of the build-time usage scan that powers the admin\n * \"requested icons\" panel — kept side-effect-free (no `fs`) so it is trivially\n * unit-testable and reusable from any walker.\n *\n * It is tag-aware, NOT a naive regex. The scanner walks the file skipping\n * strings and comments, and for every `<Icon …>` opening tag it parses the\n * attribute list, so a `>` inside an expression (`check={a > b}`) or a `name`\n * substring inside a sibling attribute (`iconName`, `data-name`) never trips\n * it.\n *\n * ONLY string-literal names are collected — the value must resolve statically:\n * - `name=\"x\"` / `name='x'` (direct attribute strings)\n * - `name={\"x\"}` / `name={'x'}` (string literal in an expression slot)\n * - `` name={`x`} `` (template literal, no interpolation)\n * Dynamic `name={expr}` (variables, concatenation, interpolated templates) is\n * intentionally skipped — a static scan cannot know its value.\n */\n\n/** A single literal `<Icon name=\"…\">` occurrence found in a file. */\nexport interface ExtractedUsage {\n /** The resolved literal icon name. */\n name: string\n /** 1-based line of the `name` value. */\n line: number\n /** 1-based column of the `name` value. */\n column: number\n}\n\nexport interface ExtractOptions {\n /**\n * JSX tag names treated as icon usages. Matched exactly against the opening\n * tag name, so `Icon` matches `<Icon …>` but not `<MyIcon …>`.\n *\n * @default ['Icon']\n */\n components?: string[]\n}\n\nconst DEFAULT_COMPONENTS = ['Icon']\n\n/** Plausible icon names only — set-row names are kebab-cased, so prose placeholders (`…`) never qualify. */\nconst PLAUSIBLE_NAME = /^[\\w-]+$/\n\nconst isWhitespace = (c: string): boolean => c === ' ' || c === '\\t' || c === '\\n' || c === '\\r' || c === '\\f' || c === '\\v'\nconst isIdentStart = (c: string): boolean => /[A-Za-z_$]/.test(c)\nconst isTagNameBoundary = (c: string | undefined): boolean => c === undefined || isWhitespace(c) || c === '/' || c === '>'\n\n/** Builds a binary-searchable index of line-start offsets for fast line/column lookup. */\nconst buildLineIndex = (source: string): number[] => {\n const starts = [0]\n for (let i = 0; i < source.length; i++) if (source[i] === '\\n') starts.push(i + 1)\n return starts\n}\n\nconst lineColAt = (lineStarts: number[], index: number): { line: number; column: number } => {\n // Binary search for the greatest line-start <= index.\n let lo = 0\n let hi = lineStarts.length - 1\n while (lo < hi) {\n const mid = (lo + hi + 1) >> 1\n if ((lineStarts[mid] as number) <= index) lo = mid\n else hi = mid - 1\n }\n return { line: lo + 1, column: index - (lineStarts[lo] as number) + 1 }\n}\n\n/** Skips a `\"…\"` / `'…'` string starting at `i` (the opening quote). Returns the index just past the closing quote. */\nconst skipString = (source: string, i: number): number => {\n const quote = source[i]\n i++\n while (i < source.length) {\n const c = source[i]\n if (c === '\\\\') {\n i += 2\n continue\n }\n if (c === quote) return i + 1\n i++\n }\n return i\n}\n\n/** Skips a `` `…` `` template (including nested `${ … }`) starting at the opening backtick. Returns the index just past the closing backtick. */\nconst skipTemplate = (source: string, i: number): number => {\n i++\n while (i < source.length) {\n const c = source[i]\n if (c === '\\\\') {\n i += 2\n continue\n }\n if (c === '`') return i + 1\n if (c === '$' && source[i + 1] === '{') {\n i = skipBraces(source, i + 1)\n continue\n }\n i++\n }\n return i\n}\n\n/** Skips a balanced `{ … }` region starting at the opening brace, honoring nested braces, strings, templates, and comments. Returns the index just past the closing brace. */\nconst skipBraces = (source: string, i: number): number => {\n let depth = 0\n while (i < source.length) {\n const c = source[i]\n if (c === '\"' || c === \"'\") {\n i = skipString(source, i)\n continue\n }\n if (c === '`') {\n i = skipTemplate(source, i)\n continue\n }\n if (c === '/' && source[i + 1] === '/') {\n i = skipLineComment(source, i)\n continue\n }\n if (c === '/' && source[i + 1] === '*') {\n i = skipBlockComment(source, i)\n continue\n }\n if (c === '{') depth++\n else if (c === '}') {\n depth--\n if (depth === 0) return i + 1\n }\n i++\n }\n return i\n}\n\nconst skipLineComment = (source: string, i: number): number => {\n i += 2\n while (i < source.length && source[i] !== '\\n') i++\n return i\n}\n\nconst skipBlockComment = (source: string, i: number): number => {\n i += 2\n while (i < source.length && !(source[i] === '*' && source[i + 1] === '/')) i++\n return Math.min(i + 2, source.length)\n}\n\n/**\n * Interprets a JSX attribute expression container's INNER text (the part\n * between `{` and `}`). Returns the literal string if the whole expression is a\n * single string or non-interpolated template literal, otherwise `null`\n * (dynamic — skipped).\n */\nconst interpretExpression = (inner: string): string | null => {\n const expr = inner.trim()\n if (expr.length < 2) return null\n const first = expr[0]\n const last = expr[expr.length - 1]\n if ((first === '\"' || first === \"'\") && last === first) {\n // Must be a SINGLE string literal — reject concatenation like `\"a\" + b`.\n if (skipString(expr, 0) !== expr.length) return null\n return unquote(expr)\n }\n if (first === '`' && last === '`') {\n if (skipTemplate(expr, 0) !== expr.length) return null\n if (expr.includes('${')) return null // interpolated — dynamic\n return expr.slice(1, -1)\n }\n return null\n}\n\n/** Decodes a quoted string literal's escapes into its runtime value. */\nconst unquote = (literal: string): string => {\n const body = literal.slice(1, -1)\n return body.replace(/\\\\(.)/g, (_, ch: string) => {\n switch (ch) {\n case 'n':\n return '\\n'\n case 't':\n return '\\t'\n case 'r':\n return '\\r'\n default:\n return ch\n }\n })\n}\n\n/**\n * Parses the attribute list of an `<Icon …>` opening tag and, if a literal\n * `name` is present, returns it with the absolute source index of its value.\n *\n * @param source full file text\n * @param start index of the first attribute char (just past the tag name)\n * @returns the literal name + value index, or `null` if absent/dynamic\n */\nconst parseNameAttr = (source: string, start: number): { name: string; index: number } | null => {\n let i = start\n while (i < source.length) {\n const c = source.charAt(i)\n if (isWhitespace(c)) {\n i++\n continue\n }\n if (c === '>') return null // end of opening tag, no name found\n if (c === '/' && source[i + 1] === '>') return null // self-closing, no name\n if (c === '{') {\n // Spread attribute `{...props}` — skip it wholesale.\n i = skipBraces(source, i)\n continue\n }\n if (!isIdentStart(c) && c !== '-') {\n i++\n continue\n }\n // Read an attribute name.\n const attrStart = i\n while (i < source.length && /[\\w:-]/.test(source.charAt(i))) i++\n const attrName = source.slice(attrStart, i)\n // Skip whitespace before a possible `=`.\n let j = i\n while (j < source.length && isWhitespace(source.charAt(j))) j++\n if (source[j] !== '=') {\n // Boolean attribute (no value) — continue scanning from here.\n i = j\n continue\n }\n j++ // past '='\n while (j < source.length && isWhitespace(source.charAt(j))) j++\n const valChar = source[j]\n let valueIndex = j\n let value: string | null\n if (valChar === '\"' || valChar === \"'\") {\n const end = skipString(source, j)\n value = unquote(source.slice(j, end))\n i = end\n } else if (valChar === '{') {\n const end = skipBraces(source, j)\n value = interpretExpression(source.slice(j + 1, end - 1))\n valueIndex = j + 1\n i = end\n } else {\n // Malformed/unexpected — bail on this tag.\n return null\n }\n if (attrName === 'name') {\n return value === null ? null : { name: value, index: valueIndex }\n }\n }\n return null\n}\n\n/**\n * Extracts every literal icon-name usage from a single file's source text.\n * See the module doc for what counts as a literal. Returns occurrences in\n * source order; de-duplication across files happens in the aggregator.\n *\n * The scan looks directly for `<Icon …>` tag starts and parses each opening tag\n * with full string/brace awareness (so the `name` value and the tag's end are\n * found correctly). It deliberately does NOT try to mask surrounding strings or\n * comments: JSX text routinely contains apostrophes (`it's`) and `//` (`http://`)\n * that a whole-file lexer would mis-read as a string/comment and skip a real\n * usage past. Instead, two surgical guards keep commentary out without a lexer:\n * a candidate whose line leads with `//` or `*` (line comments, JSDoc\n * continuations — real JSX never starts a line that way) is skipped, and\n * extracted names must look like icon names ({@link PLAUSIBLE_NAME}), so prose\n * placeholders like `…` are dropped. An `<Icon name=\"…\">` written literally in\n * a JS string (or a mid-line comment) can still be picked up — a harmless\n * over-count for an inventory, where missing a real usage would be the worse\n * failure.\n */\nexport const extractIconUsages = (source: string, options: ExtractOptions = {}): ExtractedUsage[] => {\n const components = options.components?.length ? options.components : DEFAULT_COMPONENTS\n const componentSet = new Set(components)\n const lineStarts = buildLineIndex(source)\n const usages: ExtractedUsage[] = []\n const n = source.length\n let i = 0\n\n while (i < n) {\n if (source[i] !== '<') {\n i++\n continue\n }\n // Candidate opening tag — read the tag name and check it against the set.\n let k = i + 1\n if (k >= n || !isIdentStart(source.charAt(k))) {\n i++\n continue\n }\n const nameStart = k\n while (k < n && /[\\w.]/.test(source.charAt(k))) k++\n const tagName = source.slice(nameStart, k)\n if (!componentSet.has(tagName) || !isTagNameBoundary(source[k])) {\n i = k\n continue\n }\n // Comment guard: only the text between the line start and this `<` counts,\n // so a URL's `//` earlier on the line never triggers a false skip.\n const linePrefix = source.slice(source.lastIndexOf('\\n', i - 1) + 1, i).trimStart()\n if (linePrefix.startsWith('//') || linePrefix.startsWith('*')) {\n i = k\n continue\n }\n const found = parseNameAttr(source, k)\n if (found && PLAUSIBLE_NAME.test(found.name.trim())) {\n const { line, column } = lineColAt(lineStarts, found.index)\n usages.push({ name: found.name.trim(), line, column })\n }\n i = k\n }\n\n return usages\n}\n"],"names":["DEFAULT_COMPONENTS","PLAUSIBLE_NAME","isWhitespace","c","isIdentStart","test","isTagNameBoundary","undefined","buildLineIndex","source","starts","i","length","push","lineColAt","lineStarts","index","lo","hi","mid","line","column","skipString","quote","skipTemplate","skipBraces","depth","skipLineComment","skipBlockComment","Math","min","interpretExpression","inner","expr","trim","first","last","unquote","includes","slice","literal","body","replace","_","ch","parseNameAttr","start","charAt","attrStart","attrName","j","valChar","valueIndex","value","end","name","extractIconUsages","options","components","componentSet","Set","usages","n","k","nameStart","tagName","has","linePrefix","lastIndexOf","trimStart","startsWith","found"],"mappings":"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"}
@@ -0,0 +1,81 @@
1
+ /**
2
+ * Build-time icon-usage scan — the Node-side entry point. Walks a project's
3
+ * source tree, extracts every literal `<Icon name="…">` (see {@link extractIconUsages}),
4
+ * and aggregates the result into an {@link IconUsageManifest} that the admin
5
+ * "requested icons" panel reads.
6
+ *
7
+ * Zero third-party dependencies: directory walking uses `node:fs` directly so
8
+ * the package adds nothing to a consumer's install for this feature.
9
+ */
10
+ import type { IconUsageManifest } from './types.js';
11
+ export { extractIconUsages } from './extract.js';
12
+ export type { ExtractedUsage, ExtractOptions } from './extract.js';
13
+ export type { IconUsage, IconUsageManifest } from './types.js';
14
+ export { DEFAULT_MANIFEST_FILENAME, loadIconUsageManifest, MANIFEST_PATH_ENV, resolveManifestPath } from './load.js';
15
+ /** Root directories scanned by default. */
16
+ export declare const DEFAULT_ROOTS: string[];
17
+ /** File extensions scanned by default — the JSX-bearing source formats. */
18
+ export declare const DEFAULT_EXTENSIONS: string[];
19
+ /** Directory names never descended into. */
20
+ export declare const DEFAULT_IGNORE: string[];
21
+ export interface ScanOptions {
22
+ /**
23
+ * Directories (or individual files) to scan, relative to {@link cwd} or
24
+ * absolute. Each directory is walked recursively.
25
+ *
26
+ * @default ['src', 'app']
27
+ */
28
+ roots?: string[];
29
+ /**
30
+ * Working directory the scan resolves {@link roots} and reports file paths
31
+ * relative to.
32
+ *
33
+ * @default process.cwd()
34
+ */
35
+ cwd?: string;
36
+ /**
37
+ * JSX tag names treated as icon usages. Forwarded to {@link extractIconUsages}.
38
+ *
39
+ * @default ['Icon']
40
+ */
41
+ components?: string[];
42
+ /**
43
+ * File extensions (without the dot) to read.
44
+ *
45
+ * @default ['tsx', 'jsx', 'ts', 'js', 'mdx']
46
+ */
47
+ extensions?: string[];
48
+ /**
49
+ * Directory names to skip while walking.
50
+ *
51
+ * @default ['node_modules', '.next', '.git', 'dist', 'build', 'coverage', '.turbo']
52
+ */
53
+ ignore?: string[];
54
+ }
55
+ /** The result of a {@link scanIconUsages} run. */
56
+ export interface ScanResult {
57
+ /** The assembled manifest, ready to write. */
58
+ manifest: IconUsageManifest;
59
+ /** Number of files read during the scan. */
60
+ filesScanned: number;
61
+ /** Number of scan roots that actually existed — `0` means every root was missing (wrong cwd?). */
62
+ rootsScanned: number;
63
+ }
64
+ /**
65
+ * Scans the project for literal icon usages and builds a manifest. Pure with
66
+ * respect to output (writes nothing) — use {@link writeIconUsageManifest} to
67
+ * persist the result.
68
+ *
69
+ * @example
70
+ * ```ts
71
+ * const { manifest } = scanIconUsages({ roots: ['src', 'app'] })
72
+ * console.log(manifest.names) // ['arrow-right', 'chevron', 'x', …]
73
+ * ```
74
+ */
75
+ export declare const scanIconUsages: (options?: ScanOptions) => ScanResult;
76
+ /**
77
+ * Writes a manifest to disk as pretty-printed JSON (trailing newline). Returns
78
+ * the absolute path written.
79
+ */
80
+ export declare const writeIconUsageManifest: (manifest: IconUsageManifest, path: string) => string;
81
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/scan/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAMH,OAAO,KAAK,EAAa,iBAAiB,EAAE,MAAM,YAAY,CAAA;AAE9D,OAAO,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAA;AAChD,YAAY,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,cAAc,CAAA;AAClE,YAAY,EAAE,SAAS,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAA;AAI9D,OAAO,EAAE,yBAAyB,EAAE,qBAAqB,EAAE,iBAAiB,EAAE,mBAAmB,EAAE,MAAM,WAAW,CAAA;AAEpH,2CAA2C;AAC3C,eAAO,MAAM,aAAa,UAAiB,CAAA;AAE3C,2EAA2E;AAC3E,eAAO,MAAM,kBAAkB,UAAoC,CAAA;AAEnE,4CAA4C;AAC5C,eAAO,MAAM,cAAc,UAA2E,CAAA;AAEtG,MAAM,WAAW,WAAW;IAC1B;;;;;OAKG;IACH,KAAK,CAAC,EAAE,MAAM,EAAE,CAAA;IAChB;;;;;OAKG;IACH,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,EAAE,CAAA;IACrB;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,EAAE,CAAA;IACrB;;;;OAIG;IACH,MAAM,CAAC,EAAE,MAAM,EAAE,CAAA;CAClB;AAuBD,kDAAkD;AAClD,MAAM,WAAW,UAAU;IACzB,8CAA8C;IAC9C,QAAQ,EAAE,iBAAiB,CAAA;IAC3B,4CAA4C;IAC5C,YAAY,EAAE,MAAM,CAAA;IACpB,kGAAkG;IAClG,YAAY,EAAE,MAAM,CAAA;CACrB;AAED;;;;;;;;;;GAUG;AACH,eAAO,MAAM,cAAc,GAAI,UAAS,WAAgB,KAAG,UA8C1D,CAAA;AAED;;;GAGG;AACH,eAAO,MAAM,sBAAsB,GAAI,UAAU,iBAAiB,EAAE,MAAM,MAAM,KAAG,MAIlF,CAAA"}
@@ -0,0 +1,133 @@
1
+ /**
2
+ * Build-time icon-usage scan — the Node-side entry point. Walks a project's
3
+ * source tree, extracts every literal `<Icon name="…">` (see {@link extractIconUsages}),
4
+ * and aggregates the result into an {@link IconUsageManifest} that the admin
5
+ * "requested icons" panel reads.
6
+ *
7
+ * Zero third-party dependencies: directory walking uses `node:fs` directly so
8
+ * the package adds nothing to a consumer's install for this feature.
9
+ */ import { readdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
10
+ import { isAbsolute, join, relative, resolve, sep } from "node:path";
11
+ import { extractIconUsages } from "./extract.js";
12
+ export { extractIconUsages } from "./extract.js";
13
+ // Manifest path resolution + reading live in ./load (a bundler-safe leaf with no
14
+ // runtime relative imports) so the admin panel can import them without pulling
15
+ // in this CLI/parser module. Re-exported here to keep the `./scan` API in one place.
16
+ export { DEFAULT_MANIFEST_FILENAME, loadIconUsageManifest, MANIFEST_PATH_ENV, resolveManifestPath } from "./load.js";
17
+ /** Root directories scanned by default. */ export const DEFAULT_ROOTS = [
18
+ 'src',
19
+ 'app'
20
+ ];
21
+ /** File extensions scanned by default — the JSX-bearing source formats. */ export const DEFAULT_EXTENSIONS = [
22
+ 'tsx',
23
+ 'jsx',
24
+ 'ts',
25
+ 'js',
26
+ 'mdx'
27
+ ];
28
+ /** Directory names never descended into. */ export const DEFAULT_IGNORE = [
29
+ 'node_modules',
30
+ '.next',
31
+ '.git',
32
+ 'dist',
33
+ 'build',
34
+ 'coverage',
35
+ '.turbo'
36
+ ];
37
+ const toPosix = (p)=>p.split(sep).join('/');
38
+ /** Recursively collects scannable file paths under `dir`. */ const walk = (dir, exts, ignore, out)=>{
39
+ let entries;
40
+ try {
41
+ entries = readdirSync(dir, {
42
+ withFileTypes: true
43
+ });
44
+ } catch {
45
+ return; // unreadable/non-existent root — skip silently, the caller reports totals
46
+ }
47
+ for (const entry of entries){
48
+ const full = join(dir, entry.name);
49
+ if (entry.isDirectory()) {
50
+ if (!ignore.has(entry.name)) walk(full, exts, ignore, out);
51
+ } else if (entry.isFile()) {
52
+ const dot = entry.name.lastIndexOf('.');
53
+ if (dot !== -1 && exts.has(entry.name.slice(dot + 1))) out.push(full);
54
+ }
55
+ }
56
+ };
57
+ /**
58
+ * Scans the project for literal icon usages and builds a manifest. Pure with
59
+ * respect to output (writes nothing) — use {@link writeIconUsageManifest} to
60
+ * persist the result.
61
+ *
62
+ * @example
63
+ * ```ts
64
+ * const { manifest } = scanIconUsages({ roots: ['src', 'app'] })
65
+ * console.log(manifest.names) // ['arrow-right', 'chevron', 'x', …]
66
+ * ```
67
+ */ export const scanIconUsages = (options = {})=>{
68
+ const cwd = options.cwd ?? process.cwd();
69
+ const roots = options.roots?.length ? options.roots : DEFAULT_ROOTS;
70
+ const exts = new Set((options.extensions?.length ? options.extensions : DEFAULT_EXTENSIONS).map((e)=>e.replace(/^\./, '')));
71
+ const ignore = new Set(options.ignore?.length ? options.ignore : DEFAULT_IGNORE);
72
+ const files = [];
73
+ let rootsScanned = 0;
74
+ for (const root of roots){
75
+ const abs = isAbsolute(root) ? root : resolve(cwd, root);
76
+ let stat = null;
77
+ try {
78
+ stat = statSync(abs);
79
+ } catch {
80
+ continue; // missing root — skip; the caller checks `rootsScanned`
81
+ }
82
+ rootsScanned++;
83
+ if (stat.isDirectory()) walk(abs, exts, ignore, files);
84
+ else if (stat.isFile()) files.push(abs);
85
+ }
86
+ const usages = [];
87
+ let filesScanned = 0;
88
+ for (const file of files){
89
+ let source;
90
+ try {
91
+ source = readFileSync(file, 'utf8');
92
+ } catch {
93
+ continue;
94
+ }
95
+ filesScanned++;
96
+ const rel = toPosix(relative(cwd, file));
97
+ for (const u of extractIconUsages(source, {
98
+ components: options.components
99
+ })){
100
+ usages.push({
101
+ name: u.name,
102
+ file: rel,
103
+ line: u.line,
104
+ column: u.column
105
+ });
106
+ }
107
+ }
108
+ // Deterministic ordering so the manifest is diff-stable across runs.
109
+ usages.sort((a, b)=>a.name.localeCompare(b.name) || a.file.localeCompare(b.file) || a.line - b.line || a.column - b.column);
110
+ const names = [
111
+ ...new Set(usages.map((u)=>u.name))
112
+ ].sort((a, b)=>a.localeCompare(b));
113
+ return {
114
+ manifest: {
115
+ version: 1,
116
+ generatedAt: new Date().toISOString(),
117
+ names,
118
+ usages
119
+ },
120
+ filesScanned,
121
+ rootsScanned
122
+ };
123
+ };
124
+ /**
125
+ * Writes a manifest to disk as pretty-printed JSON (trailing newline). Returns
126
+ * the absolute path written.
127
+ */ export const writeIconUsageManifest = (manifest, path)=>{
128
+ const abs = isAbsolute(path) ? path : resolve(process.cwd(), path);
129
+ writeFileSync(abs, `${JSON.stringify(manifest, null, 2)}\n`, 'utf8');
130
+ return abs;
131
+ };
132
+
133
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/scan/index.ts"],"sourcesContent":["/**\n * Build-time icon-usage scan — the Node-side entry point. Walks a project's\n * source tree, extracts every literal `<Icon name=\"…\">` (see {@link extractIconUsages}),\n * and aggregates the result into an {@link IconUsageManifest} that the admin\n * \"requested icons\" panel reads.\n *\n * Zero third-party dependencies: directory walking uses `node:fs` directly so\n * the package adds nothing to a consumer's install for this feature.\n */\n\nimport { type Dirent, readdirSync, readFileSync, statSync, writeFileSync } from 'node:fs'\nimport { isAbsolute, join, relative, resolve, sep } from 'node:path'\n\nimport { extractIconUsages } from './extract.js'\nimport type { IconUsage, IconUsageManifest } from './types.js'\n\nexport { extractIconUsages } from './extract.js'\nexport type { ExtractedUsage, ExtractOptions } from './extract.js'\nexport type { IconUsage, IconUsageManifest } from './types.js'\n// Manifest path resolution + reading live in ./load (a bundler-safe leaf with no\n// runtime relative imports) so the admin panel can import them without pulling\n// in this CLI/parser module. Re-exported here to keep the `./scan` API in one place.\nexport { DEFAULT_MANIFEST_FILENAME, loadIconUsageManifest, MANIFEST_PATH_ENV, resolveManifestPath } from './load.js'\n\n/** Root directories scanned by default. */\nexport const DEFAULT_ROOTS = ['src', 'app']\n\n/** File extensions scanned by default — the JSX-bearing source formats. */\nexport const DEFAULT_EXTENSIONS = ['tsx', 'jsx', 'ts', 'js', 'mdx']\n\n/** Directory names never descended into. */\nexport const DEFAULT_IGNORE = ['node_modules', '.next', '.git', 'dist', 'build', 'coverage', '.turbo']\n\nexport interface ScanOptions {\n /**\n * Directories (or individual files) to scan, relative to {@link cwd} or\n * absolute. Each directory is walked recursively.\n *\n * @default ['src', 'app']\n */\n roots?: string[]\n /**\n * Working directory the scan resolves {@link roots} and reports file paths\n * relative to.\n *\n * @default process.cwd()\n */\n cwd?: string\n /**\n * JSX tag names treated as icon usages. Forwarded to {@link extractIconUsages}.\n *\n * @default ['Icon']\n */\n components?: string[]\n /**\n * File extensions (without the dot) to read.\n *\n * @default ['tsx', 'jsx', 'ts', 'js', 'mdx']\n */\n extensions?: string[]\n /**\n * Directory names to skip while walking.\n *\n * @default ['node_modules', '.next', '.git', 'dist', 'build', 'coverage', '.turbo']\n */\n ignore?: string[]\n}\n\nconst toPosix = (p: string): string => p.split(sep).join('/')\n\n/** Recursively collects scannable file paths under `dir`. */\nconst walk = (dir: string, exts: Set<string>, ignore: Set<string>, out: string[]): void => {\n let entries: Dirent[]\n try {\n entries = readdirSync(dir, { withFileTypes: true })\n } catch {\n return // unreadable/non-existent root — skip silently, the caller reports totals\n }\n for (const entry of entries) {\n const full = join(dir, entry.name)\n if (entry.isDirectory()) {\n if (!ignore.has(entry.name)) walk(full, exts, ignore, out)\n } else if (entry.isFile()) {\n const dot = entry.name.lastIndexOf('.')\n if (dot !== -1 && exts.has(entry.name.slice(dot + 1))) out.push(full)\n }\n }\n}\n\n/** The result of a {@link scanIconUsages} run. */\nexport interface ScanResult {\n /** The assembled manifest, ready to write. */\n manifest: IconUsageManifest\n /** Number of files read during the scan. */\n filesScanned: number\n /** Number of scan roots that actually existed — `0` means every root was missing (wrong cwd?). */\n rootsScanned: number\n}\n\n/**\n * Scans the project for literal icon usages and builds a manifest. Pure with\n * respect to output (writes nothing) — use {@link writeIconUsageManifest} to\n * persist the result.\n *\n * @example\n * ```ts\n * const { manifest } = scanIconUsages({ roots: ['src', 'app'] })\n * console.log(manifest.names) // ['arrow-right', 'chevron', 'x', …]\n * ```\n */\nexport const scanIconUsages = (options: ScanOptions = {}): ScanResult => {\n const cwd = options.cwd ?? process.cwd()\n const roots = options.roots?.length ? options.roots : DEFAULT_ROOTS\n const exts = new Set((options.extensions?.length ? options.extensions : DEFAULT_EXTENSIONS).map((e) => e.replace(/^\\./, '')))\n const ignore = new Set(options.ignore?.length ? options.ignore : DEFAULT_IGNORE)\n\n const files: string[] = []\n let rootsScanned = 0\n for (const root of roots) {\n const abs = isAbsolute(root) ? root : resolve(cwd, root)\n let stat: ReturnType<typeof statSync> | null = null\n try {\n stat = statSync(abs)\n } catch {\n continue // missing root — skip; the caller checks `rootsScanned`\n }\n rootsScanned++\n if (stat.isDirectory()) walk(abs, exts, ignore, files)\n else if (stat.isFile()) files.push(abs)\n }\n\n const usages: IconUsage[] = []\n let filesScanned = 0\n for (const file of files) {\n let source: string\n try {\n source = readFileSync(file, 'utf8')\n } catch {\n continue\n }\n filesScanned++\n const rel = toPosix(relative(cwd, file))\n for (const u of extractIconUsages(source, { components: options.components })) {\n usages.push({ name: u.name, file: rel, line: u.line, column: u.column })\n }\n }\n\n // Deterministic ordering so the manifest is diff-stable across runs.\n usages.sort((a, b) => a.name.localeCompare(b.name) || a.file.localeCompare(b.file) || a.line - b.line || a.column - b.column)\n const names = [...new Set(usages.map((u) => u.name))].sort((a, b) => a.localeCompare(b))\n\n return {\n manifest: { version: 1, generatedAt: new Date().toISOString(), names, usages },\n filesScanned,\n rootsScanned,\n }\n}\n\n/**\n * Writes a manifest to disk as pretty-printed JSON (trailing newline). Returns\n * the absolute path written.\n */\nexport const writeIconUsageManifest = (manifest: IconUsageManifest, path: string): string => {\n const abs = isAbsolute(path) ? path : resolve(process.cwd(), path)\n writeFileSync(abs, `${JSON.stringify(manifest, null, 2)}\\n`, 'utf8')\n return abs\n}\n"],"names":["readdirSync","readFileSync","statSync","writeFileSync","isAbsolute","join","relative","resolve","sep","extractIconUsages","DEFAULT_MANIFEST_FILENAME","loadIconUsageManifest","MANIFEST_PATH_ENV","resolveManifestPath","DEFAULT_ROOTS","DEFAULT_EXTENSIONS","DEFAULT_IGNORE","toPosix","p","split","walk","dir","exts","ignore","out","entries","withFileTypes","entry","full","name","isDirectory","has","isFile","dot","lastIndexOf","slice","push","scanIconUsages","options","cwd","process","roots","length","Set","extensions","map","e","replace","files","rootsScanned","root","abs","stat","usages","filesScanned","file","source","rel","u","components","line","column","sort","a","b","localeCompare","names","manifest","version","generatedAt","Date","toISOString","writeIconUsageManifest","path","JSON","stringify"],"mappings":"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"}
@@ -0,0 +1,20 @@
1
+ import type { IconUsageManifest } from './types';
2
+ export interface LiveScanOptions {
3
+ /** Directories/files to scan, relative to {@link cwd} or absolute. @default ['src', 'app'] */
4
+ roots?: string[];
5
+ /** Working directory. @default process.cwd() */
6
+ cwd?: string;
7
+ /** JSX tag names treated as icon usages. @default ['Icon'] */
8
+ components?: string[];
9
+ /** File extensions (without the dot) to read. @default ['tsx', 'jsx', 'ts', 'js', 'mdx'] */
10
+ extensions?: string[];
11
+ /** Directory names to skip. @default ['node_modules', '.next', '.git', 'dist', 'build', 'coverage', '.turbo'] */
12
+ ignore?: string[];
13
+ }
14
+ /**
15
+ * Scan the source tree and return a manifest — the same shape the CLI writes,
16
+ * built in memory. Returns an empty manifest (no throw) when the roots don't
17
+ * exist, so it's safe to call at admin-render time.
18
+ */
19
+ export declare const scanIconUsagesLive: (options?: LiveScanOptions) => IconUsageManifest;
20
+ //# sourceMappingURL=live.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"live.d.ts","sourceRoot":"","sources":["../../src/scan/live.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAa,iBAAiB,EAAE,MAAM,SAAS,CAAA;AAmB3D,MAAM,WAAW,eAAe;IAC9B,8FAA8F;IAC9F,KAAK,CAAC,EAAE,MAAM,EAAE,CAAA;IAChB,gDAAgD;IAChD,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,8DAA8D;IAC9D,UAAU,CAAC,EAAE,MAAM,EAAE,CAAA;IACrB,4FAA4F;IAC5F,UAAU,CAAC,EAAE,MAAM,EAAE,CAAA;IACrB,iHAAiH;IACjH,MAAM,CAAC,EAAE,MAAM,EAAE,CAAA;CAClB;AAuBD;;;;GAIG;AACH,eAAO,MAAM,kBAAkB,GAAI,UAAS,eAAoB,KAAG,iBAqClE,CAAA"}