@sigx/lynx-plugin 0.4.0 → 0.4.1
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.js +226 -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 +13 -11
- 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,226 @@
|
|
|
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 set="…" name="…" />` usages.
|
|
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.
|
|
22
|
+
*/
|
|
23
|
+
import { createRequire } from 'node:module';
|
|
24
|
+
import { promises as fs, existsSync } from 'node:fs';
|
|
25
|
+
import { join } from 'node:path';
|
|
26
|
+
import { pathToFileURL } from 'node:url';
|
|
27
|
+
const SCAN_REGEX_SET_FIRST = /<Icon\s+[^>]*?\bset\s*=\s*["']([\w-]+)["'][^>]*?\bname\s*=\s*["']([\w-]+)["']/g;
|
|
28
|
+
const SCAN_REGEX_NAME_FIRST = /<Icon\s+[^>]*?\bname\s*=\s*["']([\w-]+)["'][^>]*?\bset\s*=\s*["']([\w-]+)["']/g;
|
|
29
|
+
/** Directories to skip when walking the project. */
|
|
30
|
+
const SKIP_DIRS = new Set(['node_modules', 'dist', 'ios', 'android', 'Pods', '.git', '.cache', '.rspeedy']);
|
|
31
|
+
/** File extensions worth scanning. */
|
|
32
|
+
const SOURCE_EXT = /\.(?:tsx?|jsx?)$/;
|
|
33
|
+
async function walkSourceFiles(root) {
|
|
34
|
+
const out = [];
|
|
35
|
+
async function walk(dir) {
|
|
36
|
+
let entries;
|
|
37
|
+
try {
|
|
38
|
+
entries = await fs.readdir(dir, { withFileTypes: true });
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
await Promise.all(entries.map(async (entry) => {
|
|
44
|
+
if (entry.name.startsWith('.'))
|
|
45
|
+
return;
|
|
46
|
+
if (SKIP_DIRS.has(entry.name))
|
|
47
|
+
return;
|
|
48
|
+
const full = join(dir, entry.name);
|
|
49
|
+
if (entry.isDirectory())
|
|
50
|
+
return walk(full);
|
|
51
|
+
if (entry.isFile() && SOURCE_EXT.test(entry.name))
|
|
52
|
+
out.push(full);
|
|
53
|
+
}));
|
|
54
|
+
}
|
|
55
|
+
await walk(root);
|
|
56
|
+
return out;
|
|
57
|
+
}
|
|
58
|
+
function addUsage(used, set, name) {
|
|
59
|
+
let bucket = used.get(set);
|
|
60
|
+
if (!bucket) {
|
|
61
|
+
bucket = new Set();
|
|
62
|
+
used.set(set, bucket);
|
|
63
|
+
}
|
|
64
|
+
bucket.add(name);
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Extract `<Icon set="…" name="…" />` (and the name-first variant) usages
|
|
68
|
+
* from a single source string. Exported for unit testing — the prod path
|
|
69
|
+
* calls this once per file from {@link scanProject}.
|
|
70
|
+
*/
|
|
71
|
+
export function scanContent(content) {
|
|
72
|
+
if (!content.includes('<Icon'))
|
|
73
|
+
return [];
|
|
74
|
+
const seen = new Set();
|
|
75
|
+
const out = [];
|
|
76
|
+
const push = (set, name) => {
|
|
77
|
+
const key = `${set}\0${name}`;
|
|
78
|
+
if (seen.has(key))
|
|
79
|
+
return;
|
|
80
|
+
seen.add(key);
|
|
81
|
+
out.push({ set, name });
|
|
82
|
+
};
|
|
83
|
+
for (const m of content.matchAll(SCAN_REGEX_SET_FIRST))
|
|
84
|
+
push(m[1], m[2]);
|
|
85
|
+
for (const m of content.matchAll(SCAN_REGEX_NAME_FIRST))
|
|
86
|
+
push(m[2], m[1]);
|
|
87
|
+
return out;
|
|
88
|
+
}
|
|
89
|
+
async function scanProject(cwd) {
|
|
90
|
+
const used = new Map();
|
|
91
|
+
const files = await walkSourceFiles(cwd);
|
|
92
|
+
await Promise.all(files.map(async (file) => {
|
|
93
|
+
const content = await fs.readFile(file, 'utf8').catch(() => '');
|
|
94
|
+
for (const { set, name } of scanContent(content))
|
|
95
|
+
addUsage(used, set, name);
|
|
96
|
+
}));
|
|
97
|
+
return used;
|
|
98
|
+
}
|
|
99
|
+
async function loadAdapter(cwd, source) {
|
|
100
|
+
try {
|
|
101
|
+
const cwdRequire = createRequire(join(cwd, 'noop.js'));
|
|
102
|
+
const adapterPath = cwdRequire.resolve(source);
|
|
103
|
+
// Wrap with file:// — Windows ESM rejects bare absolute paths in dynamic
|
|
104
|
+
// import(). Same pattern as packages/lynx-cli/src/prebuild.ts loadConfig.
|
|
105
|
+
const mod = (await import(pathToFileURL(adapterPath).href));
|
|
106
|
+
return mod.default ?? mod;
|
|
107
|
+
}
|
|
108
|
+
catch (err) {
|
|
109
|
+
// Adapter not installed — silently skip; consumer will see missing-icon placeholders.
|
|
110
|
+
if (process.env['SIGX_DEBUG_ICONS']) {
|
|
111
|
+
// eslint-disable-next-line no-console
|
|
112
|
+
console.warn(`[@sigx/lynx-plugin] icons: failed to load adapter "${source}":`, err);
|
|
113
|
+
}
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
function collectGlyphsForSet(adapter, setConfig, usedNames) {
|
|
118
|
+
const codepoints = {};
|
|
119
|
+
const svgs = {};
|
|
120
|
+
const stylesToTry = setConfig.styles ?? adapter.styles;
|
|
121
|
+
// v1 contract: codepoint and svg are MUTUALLY EXCLUSIVE per set so the
|
|
122
|
+
// runtime can pick a render strategy without ambiguity. v1 only ships
|
|
123
|
+
// SVG mode (no @font-face CSS generation yet), so we never emit
|
|
124
|
+
// codepoints — even when the adapter returns them. v1.1's font-mode work
|
|
125
|
+
// flips the switch by emitting codepoints + matching @font-face CSS for
|
|
126
|
+
// sets whose `mode` is 'font'.
|
|
127
|
+
const emitCodepoints = false;
|
|
128
|
+
for (const name of usedNames) {
|
|
129
|
+
let glyph = null;
|
|
130
|
+
for (const style of stylesToTry) {
|
|
131
|
+
glyph = adapter.getGlyph(style, name);
|
|
132
|
+
if (glyph)
|
|
133
|
+
break;
|
|
134
|
+
}
|
|
135
|
+
if (!glyph)
|
|
136
|
+
continue;
|
|
137
|
+
if (emitCodepoints && glyph.codepoint !== undefined) {
|
|
138
|
+
codepoints[name] = glyph.codepoint;
|
|
139
|
+
}
|
|
140
|
+
else {
|
|
141
|
+
svgs[name] = { svg: glyph.svg };
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return { codepoints, svgs };
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Wire `@sigx/lynx-icons` adapter packages declared in `signalx.config.ts`.
|
|
148
|
+
* Called from {@link pluginSigxLynx}'s `setup()` after the dev/asset patches.
|
|
149
|
+
*/
|
|
150
|
+
export async function applyIcons(api, opts = {}) {
|
|
151
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
152
|
+
// Two layered concerns:
|
|
153
|
+
// 1. No config / lynx-cli not installed → silent no-op (genuine non-Lynx context).
|
|
154
|
+
// 2. Config exists but loadConfig / resolveConfig throws → surface the error so a
|
|
155
|
+
// typo in iconSets (duplicate ids, unknown styles/modes) doesn't silently
|
|
156
|
+
// swallow itself and leave the user wondering why icons render as placeholders.
|
|
157
|
+
const configCandidates = [
|
|
158
|
+
'signalx.config.ts',
|
|
159
|
+
'signalx.config.js',
|
|
160
|
+
'signalx.config.mjs',
|
|
161
|
+
];
|
|
162
|
+
const hasConfig = configCandidates.some((f) => existsSync(join(cwd, f)));
|
|
163
|
+
if (!hasConfig)
|
|
164
|
+
return;
|
|
165
|
+
let cli;
|
|
166
|
+
try {
|
|
167
|
+
cli = (await import('@sigx/lynx-cli'));
|
|
168
|
+
}
|
|
169
|
+
catch {
|
|
170
|
+
// @sigx/lynx-cli is an optional peer dep; consumer outside a sigx-lynx app — skip.
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
// From here errors are real (bad config / failed validation) — let them throw
|
|
174
|
+
// so the build fails loudly. eslint-disable + console.error keeps the message
|
|
175
|
+
// visible even when the throw is wrapped by rsbuild.
|
|
176
|
+
const raw = await cli.loadConfig(cwd);
|
|
177
|
+
const config = cli.resolveConfig(raw);
|
|
178
|
+
if (!config.iconSets || config.iconSets.length === 0)
|
|
179
|
+
return;
|
|
180
|
+
const used = await scanProject(cwd);
|
|
181
|
+
const codepointsMap = {};
|
|
182
|
+
const svgsMap = {};
|
|
183
|
+
for (const setConfig of config.iconSets) {
|
|
184
|
+
const adapter = await loadAdapter(cwd, setConfig.source);
|
|
185
|
+
if (!adapter)
|
|
186
|
+
continue;
|
|
187
|
+
const setUsed = new Set(used.get(setConfig.id) ?? []);
|
|
188
|
+
for (const forced of setConfig.include)
|
|
189
|
+
setUsed.add(forced);
|
|
190
|
+
// `include: ['*']` → ship the full glyph catalog for each configured
|
|
191
|
+
// style. Required for JSON-driven UIs where icon names are unknown
|
|
192
|
+
// at build time. Trade-off: bundle grows by hundreds of KB.
|
|
193
|
+
if (setConfig.include.includes('*')) {
|
|
194
|
+
setUsed.delete('*');
|
|
195
|
+
const stylesToTry = setConfig.styles ?? adapter.styles;
|
|
196
|
+
for (const style of stylesToTry) {
|
|
197
|
+
for (const name of adapter.listGlyphs(style))
|
|
198
|
+
setUsed.add(name);
|
|
199
|
+
}
|
|
200
|
+
// eslint-disable-next-line no-console
|
|
201
|
+
console.log(`[@sigx/lynx-plugin] icons: ${setConfig.id} bundling ${setUsed.size} glyphs (include: ['*'])`);
|
|
202
|
+
}
|
|
203
|
+
if (setUsed.size === 0)
|
|
204
|
+
continue;
|
|
205
|
+
const { codepoints, svgs } = collectGlyphsForSet(adapter, setConfig, setUsed);
|
|
206
|
+
if (Object.keys(codepoints).length > 0)
|
|
207
|
+
codepointsMap[setConfig.id] = codepoints;
|
|
208
|
+
if (Object.keys(svgs).length > 0)
|
|
209
|
+
svgsMap[setConfig.id] = svgs;
|
|
210
|
+
}
|
|
211
|
+
// Persist generated modules into the project's pnpm cache dir.
|
|
212
|
+
const cacheDir = join(cwd, 'node_modules', '.cache', 'sigx-lynx-icons');
|
|
213
|
+
await fs.mkdir(cacheDir, { recursive: true });
|
|
214
|
+
const codepointsPath = join(cacheDir, 'codepoints.mjs');
|
|
215
|
+
const svgsPath = join(cacheDir, 'svgs.mjs');
|
|
216
|
+
const fontFacePath = join(cacheDir, 'font-face.css');
|
|
217
|
+
await fs.writeFile(codepointsPath, `// Auto-generated by @sigx/lynx-plugin — do not edit.\nexport const codepoints = ${JSON.stringify(codepointsMap)};\n`);
|
|
218
|
+
await fs.writeFile(svgsPath, `// Auto-generated by @sigx/lynx-plugin — do not edit.\nexport const svgs = ${JSON.stringify(svgsMap)};\n`);
|
|
219
|
+
await fs.writeFile(fontFacePath, '/* Auto-generated by @sigx/lynx-plugin — font mode lands in v1.1. */\n');
|
|
220
|
+
// Alias the three subpath imports to the generated files.
|
|
221
|
+
api.modifyBundlerChain((chain) => {
|
|
222
|
+
chain.resolve.alias.set('@sigx/lynx-icons/__codepoints', codepointsPath);
|
|
223
|
+
chain.resolve.alias.set('@sigx/lynx-icons/__svgs', svgsPath);
|
|
224
|
+
chain.resolve.alias.set('@sigx/lynx-icons/__font-face.css', fontFacePath);
|
|
225
|
+
});
|
|
226
|
+
}
|
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}.
|