@sigx/lynx-plugin 0.4.0 → 0.4.2
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/dist/css.js +150 -0
- package/dist/entry.js +443 -0
- package/dist/icons.d.ts +25 -5
- package/dist/icons.js +301 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +395 -434
- package/dist/layers.js +5 -0
- package/dist/loaders/hmr-loader.js +70 -19
- package/dist/loaders/ignore-css-loader.js +16 -7
- package/dist/loaders/worklet-loader-mt.js +142 -62
- package/dist/loaders/worklet-loader.js +69 -31
- package/dist/loaders/worklet-utils.d.ts +0 -15
- package/dist/loaders/worklet-utils.js +116 -0
- package/dist/log-server.d.ts +0 -0
- package/dist/log-server.js +0 -0
- package/package.json +14 -12
- package/dist/index.js.map +0 -1
- package/dist/loaders/hmr-loader.js.map +0 -1
- package/dist/loaders/ignore-css-loader.js.map +0 -1
- package/dist/loaders/worklet-loader-mt.js.map +0 -1
- package/dist/loaders/worklet-loader.js.map +0 -1
package/dist/icons.js
ADDED
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Icon-set integration for @sigx/lynx-icons.
|
|
3
|
+
*
|
|
4
|
+
* At plugin setup time this slice:
|
|
5
|
+
*
|
|
6
|
+
* 1. Loads `signalx.config.ts` and reads the `iconSets: [...]` field.
|
|
7
|
+
* 2. Statically scans every `.tsx` / `.jsx` / `.ts` / `.js` file under the
|
|
8
|
+
* project root for icon usages (see `scanContent` for the exact patterns).
|
|
9
|
+
* 3. Dynamically imports each adapter package (e.g. `@sigx/lynx-icons-fa-free`)
|
|
10
|
+
* and resolves the used glyphs to `{ codepoint, svg }` records.
|
|
11
|
+
* 4. Writes three generated files into `node_modules/.cache/sigx-lynx-icons/`
|
|
12
|
+
* and aliases the `@sigx/lynx-icons/__codepoints` / `__svgs` / `__font-face.css`
|
|
13
|
+
* subpath imports to them, so Rspack tree-shakes everything else away.
|
|
14
|
+
*
|
|
15
|
+
* v1 emits SVG-mode artefacts only. Font-mode (build-time TTF subsetting +
|
|
16
|
+
* base64-inlined @font-face) is a v1.1 follow-up; for now the generated
|
|
17
|
+
* font-face.css is empty.
|
|
18
|
+
*
|
|
19
|
+
* The scanner is a one-shot regex pass at plugin start — adding a new icon
|
|
20
|
+
* during `pnpm dev` requires a dev-server restart in v1. A real SWC-AST
|
|
21
|
+
* Rspack loader is the planned upgrade and would obviate the regex
|
|
22
|
+
* patterns by inspecting the JSX tree directly.
|
|
23
|
+
*
|
|
24
|
+
* **Patterns the scanner picks up (regex-based; not exhaustive):**
|
|
25
|
+
* - `<Icon set="X" name="Y" />` — both attribute orders
|
|
26
|
+
* - `<FaSolidIcon name="Y" />` / `<FaRegularIcon name="Y" />`
|
|
27
|
+
* / `<FaBrandIcon name="Y" />` / `<LucideIcon name="Y" />` — pinned
|
|
28
|
+
* components whose set id is hardcoded in their implementations. The
|
|
29
|
+
* set id mapping is in `PINNED_COMPONENTS` below.
|
|
30
|
+
* - `{ set: 'X', name: 'Y' }` — `IconSpec` object literals anywhere
|
|
31
|
+
* (prop value, const declaration, function argument). Both key orders.
|
|
32
|
+
*
|
|
33
|
+
* **What still needs `include: [...]` in signalx.config.ts:**
|
|
34
|
+
* - Dynamic names: `<Icon set="fas" name={someVar} />` or
|
|
35
|
+
* `<FaSolidIcon name={someVar} />` — the scanner only matches literal
|
|
36
|
+
* string attributes. JSON-driven UIs and runtime-computed icon names
|
|
37
|
+
* need explicit force-includes (or `include: ['*']` for the whole catalog).
|
|
38
|
+
* - User-defined pinned components — only the four built-in adapter
|
|
39
|
+
* pinned components are known to the scanner. A consumer who writes
|
|
40
|
+
* their own `<MyIcon name="…">` wrapper needs `include`.
|
|
41
|
+
* - Spread props: `<Icon {...spec} />`. Niche; use `include` if needed.
|
|
42
|
+
*/
|
|
43
|
+
import { createRequire } from 'node:module';
|
|
44
|
+
import { promises as fs, existsSync } from 'node:fs';
|
|
45
|
+
import { join } from 'node:path';
|
|
46
|
+
import { pathToFileURL } from 'node:url';
|
|
47
|
+
const SCAN_REGEX_SET_FIRST = /<Icon\s+[^>]*?\bset\s*=\s*["']([\w-]+)["'][^>]*?\bname\s*=\s*["']([\w-]+)["']/g;
|
|
48
|
+
const SCAN_REGEX_NAME_FIRST = /<Icon\s+[^>]*?\bname\s*=\s*["']([\w-]+)["'][^>]*?\bset\s*=\s*["']([\w-]+)["']/g;
|
|
49
|
+
/**
|
|
50
|
+
* Known pinned per-set components exported by the workspace's adapter
|
|
51
|
+
* packages — `@sigx/lynx-icons-fa-free/components` and
|
|
52
|
+
* `@sigx/lynx-icons-lucide/components`. Each hardcodes its `set` id to
|
|
53
|
+
* the conventional value documented in the adapter's README; the
|
|
54
|
+
* scanner mirrors that mapping so `<FaSolidIcon name="user" />` is
|
|
55
|
+
* recognized as `set="fas", name="user"`.
|
|
56
|
+
*
|
|
57
|
+
* Consumers using non-conventional set ids in their config fall back
|
|
58
|
+
* to generic `<Icon>` (the pinned component wouldn't find its set at
|
|
59
|
+
* runtime either). New adapter packages adding pinned components add
|
|
60
|
+
* their entries here; the eventual SWC-AST loader replaces this with
|
|
61
|
+
* a per-package manifest.
|
|
62
|
+
*/
|
|
63
|
+
const PINNED_COMPONENTS = {
|
|
64
|
+
FaSolidIcon: 'fas',
|
|
65
|
+
FaRegularIcon: 'far',
|
|
66
|
+
FaBrandIcon: 'fab',
|
|
67
|
+
LucideIcon: 'lucide',
|
|
68
|
+
};
|
|
69
|
+
const PINNED_COMPONENT_NAMES = Object.keys(PINNED_COMPONENTS).join('|');
|
|
70
|
+
const SCAN_REGEX_PINNED = new RegExp(`<(${PINNED_COMPONENT_NAMES})\\s+[^>]*?\\bname\\s*=\\s*["']([\\w-]+)["']`, 'g');
|
|
71
|
+
/**
|
|
72
|
+
* `IconSpec` object literal matchers — `{ set: 'X', name: 'Y' }` in
|
|
73
|
+
* either key order. Used for `<Tabs.Screen icon={{…}}>`, `<NavHeader
|
|
74
|
+
* backIcon={{…}}>`, `const spec = {…}` const declarations, function
|
|
75
|
+
* arguments, etc. Word-boundary anchored on the *first* key to avoid
|
|
76
|
+
* matching mid-identifier (e.g. `someset:`). False positives — any
|
|
77
|
+
* code object with both `set` and `name` string-valued keys — are
|
|
78
|
+
* harmless: the extra glyph just ships in the bundle.
|
|
79
|
+
*/
|
|
80
|
+
const SCAN_REGEX_SPEC_SET_FIRST = /\bset\s*:\s*["']([\w-]+)["']\s*,\s*name\s*:\s*["']([\w-]+)["']/g;
|
|
81
|
+
const SCAN_REGEX_SPEC_NAME_FIRST = /\bname\s*:\s*["']([\w-]+)["']\s*,\s*set\s*:\s*["']([\w-]+)["']/g;
|
|
82
|
+
/** Directories to skip when walking the project. */
|
|
83
|
+
const SKIP_DIRS = new Set(['node_modules', 'dist', 'ios', 'android', 'Pods', '.git', '.cache', '.rspeedy']);
|
|
84
|
+
/** File extensions worth scanning. */
|
|
85
|
+
const SOURCE_EXT = /\.(?:tsx?|jsx?)$/;
|
|
86
|
+
async function walkSourceFiles(root) {
|
|
87
|
+
const out = [];
|
|
88
|
+
async function walk(dir) {
|
|
89
|
+
let entries;
|
|
90
|
+
try {
|
|
91
|
+
entries = await fs.readdir(dir, { withFileTypes: true });
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
await Promise.all(entries.map(async (entry) => {
|
|
97
|
+
if (entry.name.startsWith('.'))
|
|
98
|
+
return;
|
|
99
|
+
if (SKIP_DIRS.has(entry.name))
|
|
100
|
+
return;
|
|
101
|
+
const full = join(dir, entry.name);
|
|
102
|
+
if (entry.isDirectory())
|
|
103
|
+
return walk(full);
|
|
104
|
+
if (entry.isFile() && SOURCE_EXT.test(entry.name))
|
|
105
|
+
out.push(full);
|
|
106
|
+
}));
|
|
107
|
+
}
|
|
108
|
+
await walk(root);
|
|
109
|
+
return out;
|
|
110
|
+
}
|
|
111
|
+
function addUsage(used, set, name) {
|
|
112
|
+
let bucket = used.get(set);
|
|
113
|
+
if (!bucket) {
|
|
114
|
+
bucket = new Set();
|
|
115
|
+
used.set(set, bucket);
|
|
116
|
+
}
|
|
117
|
+
bucket.add(name);
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Extract icon usages from a single source string. See the file-level
|
|
121
|
+
* JSDoc for the complete pattern list. Exported for unit testing — the
|
|
122
|
+
* prod path calls this once per file from {@link scanProject}.
|
|
123
|
+
*/
|
|
124
|
+
export function scanContent(content) {
|
|
125
|
+
// Fast-path skip: a file with none of these markers can't possibly
|
|
126
|
+
// contain an icon usage we'd match. `set:` covers both attribute and
|
|
127
|
+
// object-literal forms; the pinned-component prefixes are listed for
|
|
128
|
+
// the JSX form.
|
|
129
|
+
if (!content.includes('<Icon')
|
|
130
|
+
&& !content.includes('<FaSolidIcon')
|
|
131
|
+
&& !content.includes('<FaRegularIcon')
|
|
132
|
+
&& !content.includes('<FaBrandIcon')
|
|
133
|
+
&& !content.includes('<LucideIcon')
|
|
134
|
+
&& !content.includes('set:')) {
|
|
135
|
+
return [];
|
|
136
|
+
}
|
|
137
|
+
const seen = new Set();
|
|
138
|
+
const out = [];
|
|
139
|
+
const push = (set, name) => {
|
|
140
|
+
const key = `${set}\0${name}`;
|
|
141
|
+
if (seen.has(key))
|
|
142
|
+
return;
|
|
143
|
+
seen.add(key);
|
|
144
|
+
out.push({ set, name });
|
|
145
|
+
};
|
|
146
|
+
// <Icon set="X" name="Y" /> — either attribute order.
|
|
147
|
+
for (const m of content.matchAll(SCAN_REGEX_SET_FIRST))
|
|
148
|
+
push(m[1], m[2]);
|
|
149
|
+
for (const m of content.matchAll(SCAN_REGEX_NAME_FIRST))
|
|
150
|
+
push(m[2], m[1]);
|
|
151
|
+
// <FaSolidIcon name="Y" /> etc. — set id resolved from PINNED_COMPONENTS.
|
|
152
|
+
for (const m of content.matchAll(SCAN_REGEX_PINNED)) {
|
|
153
|
+
const set = PINNED_COMPONENTS[m[1]];
|
|
154
|
+
if (set)
|
|
155
|
+
push(set, m[2]);
|
|
156
|
+
}
|
|
157
|
+
// { set: 'X', name: 'Y' } — IconSpec object literal, either key order.
|
|
158
|
+
for (const m of content.matchAll(SCAN_REGEX_SPEC_SET_FIRST))
|
|
159
|
+
push(m[1], m[2]);
|
|
160
|
+
for (const m of content.matchAll(SCAN_REGEX_SPEC_NAME_FIRST))
|
|
161
|
+
push(m[2], m[1]);
|
|
162
|
+
return out;
|
|
163
|
+
}
|
|
164
|
+
async function scanProject(cwd) {
|
|
165
|
+
const used = new Map();
|
|
166
|
+
const files = await walkSourceFiles(cwd);
|
|
167
|
+
await Promise.all(files.map(async (file) => {
|
|
168
|
+
const content = await fs.readFile(file, 'utf8').catch(() => '');
|
|
169
|
+
for (const { set, name } of scanContent(content))
|
|
170
|
+
addUsage(used, set, name);
|
|
171
|
+
}));
|
|
172
|
+
return used;
|
|
173
|
+
}
|
|
174
|
+
async function loadAdapter(cwd, source) {
|
|
175
|
+
try {
|
|
176
|
+
const cwdRequire = createRequire(join(cwd, 'noop.js'));
|
|
177
|
+
const adapterPath = cwdRequire.resolve(source);
|
|
178
|
+
// Wrap with file:// — Windows ESM rejects bare absolute paths in dynamic
|
|
179
|
+
// import(). Same pattern as packages/lynx-cli/src/prebuild.ts loadConfig.
|
|
180
|
+
const mod = (await import(pathToFileURL(adapterPath).href));
|
|
181
|
+
return mod.default ?? mod;
|
|
182
|
+
}
|
|
183
|
+
catch (err) {
|
|
184
|
+
// Adapter not installed — silently skip; consumer will see missing-icon placeholders.
|
|
185
|
+
if (process.env['SIGX_DEBUG_ICONS']) {
|
|
186
|
+
// eslint-disable-next-line no-console
|
|
187
|
+
console.warn(`[@sigx/lynx-plugin] icons: failed to load adapter "${source}":`, err);
|
|
188
|
+
}
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
function collectGlyphsForSet(adapter, setConfig, usedNames) {
|
|
193
|
+
const codepoints = {};
|
|
194
|
+
const svgs = {};
|
|
195
|
+
const stylesToTry = setConfig.styles ?? adapter.styles;
|
|
196
|
+
// v1 contract: codepoint and svg are MUTUALLY EXCLUSIVE per set so the
|
|
197
|
+
// runtime can pick a render strategy without ambiguity. v1 only ships
|
|
198
|
+
// SVG mode (no @font-face CSS generation yet), so we never emit
|
|
199
|
+
// codepoints — even when the adapter returns them. v1.1's font-mode work
|
|
200
|
+
// flips the switch by emitting codepoints + matching @font-face CSS for
|
|
201
|
+
// sets whose `mode` is 'font'.
|
|
202
|
+
const emitCodepoints = false;
|
|
203
|
+
for (const name of usedNames) {
|
|
204
|
+
let glyph = null;
|
|
205
|
+
for (const style of stylesToTry) {
|
|
206
|
+
glyph = adapter.getGlyph(style, name);
|
|
207
|
+
if (glyph)
|
|
208
|
+
break;
|
|
209
|
+
}
|
|
210
|
+
if (!glyph)
|
|
211
|
+
continue;
|
|
212
|
+
if (emitCodepoints && glyph.codepoint !== undefined) {
|
|
213
|
+
codepoints[name] = glyph.codepoint;
|
|
214
|
+
}
|
|
215
|
+
else {
|
|
216
|
+
svgs[name] = { svg: glyph.svg };
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
return { codepoints, svgs };
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Wire `@sigx/lynx-icons` adapter packages declared in `signalx.config.ts`.
|
|
223
|
+
* Called from {@link pluginSigxLynx}'s `setup()` after the dev/asset patches.
|
|
224
|
+
*/
|
|
225
|
+
export async function applyIcons(api, opts = {}) {
|
|
226
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
227
|
+
// Two layered concerns:
|
|
228
|
+
// 1. No config / lynx-cli not installed → silent no-op (genuine non-Lynx context).
|
|
229
|
+
// 2. Config exists but loadConfig / resolveConfig throws → surface the error so a
|
|
230
|
+
// typo in iconSets (duplicate ids, unknown styles/modes) doesn't silently
|
|
231
|
+
// swallow itself and leave the user wondering why icons render as placeholders.
|
|
232
|
+
const configCandidates = [
|
|
233
|
+
'signalx.config.ts',
|
|
234
|
+
'signalx.config.js',
|
|
235
|
+
'signalx.config.mjs',
|
|
236
|
+
];
|
|
237
|
+
const hasConfig = configCandidates.some((f) => existsSync(join(cwd, f)));
|
|
238
|
+
if (!hasConfig)
|
|
239
|
+
return;
|
|
240
|
+
let cli;
|
|
241
|
+
try {
|
|
242
|
+
cli = (await import('@sigx/lynx-cli'));
|
|
243
|
+
}
|
|
244
|
+
catch {
|
|
245
|
+
// @sigx/lynx-cli is an optional peer dep; consumer outside a sigx-lynx app — skip.
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
// From here errors are real (bad config / failed validation) — let them throw
|
|
249
|
+
// so the build fails loudly. eslint-disable + console.error keeps the message
|
|
250
|
+
// visible even when the throw is wrapped by rsbuild.
|
|
251
|
+
const raw = await cli.loadConfig(cwd);
|
|
252
|
+
const config = cli.resolveConfig(raw);
|
|
253
|
+
if (!config.iconSets || config.iconSets.length === 0)
|
|
254
|
+
return;
|
|
255
|
+
const used = await scanProject(cwd);
|
|
256
|
+
const codepointsMap = {};
|
|
257
|
+
const svgsMap = {};
|
|
258
|
+
for (const setConfig of config.iconSets) {
|
|
259
|
+
const adapter = await loadAdapter(cwd, setConfig.source);
|
|
260
|
+
if (!adapter)
|
|
261
|
+
continue;
|
|
262
|
+
const setUsed = new Set(used.get(setConfig.id) ?? []);
|
|
263
|
+
for (const forced of setConfig.include)
|
|
264
|
+
setUsed.add(forced);
|
|
265
|
+
// `include: ['*']` → ship the full glyph catalog for each configured
|
|
266
|
+
// style. Required for JSON-driven UIs where icon names are unknown
|
|
267
|
+
// at build time. Trade-off: bundle grows by hundreds of KB.
|
|
268
|
+
if (setConfig.include.includes('*')) {
|
|
269
|
+
setUsed.delete('*');
|
|
270
|
+
const stylesToTry = setConfig.styles ?? adapter.styles;
|
|
271
|
+
for (const style of stylesToTry) {
|
|
272
|
+
for (const name of adapter.listGlyphs(style))
|
|
273
|
+
setUsed.add(name);
|
|
274
|
+
}
|
|
275
|
+
// eslint-disable-next-line no-console
|
|
276
|
+
console.log(`[@sigx/lynx-plugin] icons: ${setConfig.id} bundling ${setUsed.size} glyphs (include: ['*'])`);
|
|
277
|
+
}
|
|
278
|
+
if (setUsed.size === 0)
|
|
279
|
+
continue;
|
|
280
|
+
const { codepoints, svgs } = collectGlyphsForSet(adapter, setConfig, setUsed);
|
|
281
|
+
if (Object.keys(codepoints).length > 0)
|
|
282
|
+
codepointsMap[setConfig.id] = codepoints;
|
|
283
|
+
if (Object.keys(svgs).length > 0)
|
|
284
|
+
svgsMap[setConfig.id] = svgs;
|
|
285
|
+
}
|
|
286
|
+
// Persist generated modules into the project's pnpm cache dir.
|
|
287
|
+
const cacheDir = join(cwd, 'node_modules', '.cache', 'sigx-lynx-icons');
|
|
288
|
+
await fs.mkdir(cacheDir, { recursive: true });
|
|
289
|
+
const codepointsPath = join(cacheDir, 'codepoints.mjs');
|
|
290
|
+
const svgsPath = join(cacheDir, 'svgs.mjs');
|
|
291
|
+
const fontFacePath = join(cacheDir, 'font-face.css');
|
|
292
|
+
await fs.writeFile(codepointsPath, `// Auto-generated by @sigx/lynx-plugin — do not edit.\nexport const codepoints = ${JSON.stringify(codepointsMap)};\n`);
|
|
293
|
+
await fs.writeFile(svgsPath, `// Auto-generated by @sigx/lynx-plugin — do not edit.\nexport const svgs = ${JSON.stringify(svgsMap)};\n`);
|
|
294
|
+
await fs.writeFile(fontFacePath, '/* Auto-generated by @sigx/lynx-plugin — font mode lands in v1.1. */\n');
|
|
295
|
+
// Alias the three subpath imports to the generated files.
|
|
296
|
+
api.modifyBundlerChain((chain) => {
|
|
297
|
+
chain.resolve.alias.set('@sigx/lynx-icons/__codepoints', codepointsPath);
|
|
298
|
+
chain.resolve.alias.set('@sigx/lynx-icons/__svgs', svgsPath);
|
|
299
|
+
chain.resolve.alias.set('@sigx/lynx-icons/__font-face.css', fontFacePath);
|
|
300
|
+
});
|
|
301
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -16,8 +16,8 @@
|
|
|
16
16
|
* ```
|
|
17
17
|
*/
|
|
18
18
|
import type { RsbuildPlugin } from '@rsbuild/core';
|
|
19
|
-
import { applyEntry } from './entry';
|
|
20
|
-
import { LAYERS } from './layers';
|
|
19
|
+
import { applyEntry } from './entry.js';
|
|
20
|
+
import { LAYERS } from './layers.js';
|
|
21
21
|
export { LAYERS, applyEntry };
|
|
22
22
|
/**
|
|
23
23
|
* Options for {@link pluginSigxLynx}.
|