@knighted/css 1.1.0 → 1.2.0-rc.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.
- package/README.md +54 -2
- package/dist/cjs/generateTypes.cjs +340 -38
- package/dist/cjs/generateTypes.cjs.map +1 -1
- package/dist/cjs/generateTypes.d.cts +33 -0
- package/dist/cjs/lexer.cjs +64 -2
- package/dist/cjs/lexer.cjs.map +1 -1
- package/dist/cjs/lexer.d.cts +1 -0
- package/dist/cjs/loaderBridge.cjs +10 -7
- package/dist/cjs/loaderBridge.cjs.map +1 -1
- package/dist/cjs/plugin.cjs +485 -0
- package/dist/cjs/plugin.cjs.map +1 -0
- package/dist/cjs/plugin.d.cts +135 -0
- package/dist/generateTypes.d.ts +33 -0
- package/dist/generateTypes.js +340 -38
- package/dist/generateTypes.js.map +1 -1
- package/dist/lexer.d.ts +1 -0
- package/dist/lexer.js +64 -2
- package/dist/lexer.js.map +1 -1
- package/dist/loaderBridge.js +10 -7
- package/dist/loaderBridge.js.map +1 -1
- package/dist/plugin.d.ts +135 -0
- package/dist/plugin.js +477 -0
- package/dist/plugin.js.map +1 -0
- package/package.json +9 -2
package/README.md
CHANGED
|
@@ -30,7 +30,7 @@ I needed a single source of truth for UI components that could drop into both li
|
|
|
30
30
|
- Deterministic selector duplication via `autoStable`: duplicate matching class selectors with a stable namespace (default `knighted-`) in both plain CSS and CSS Modules exports.
|
|
31
31
|
- Pluggable resolver/filter hooks for custom module resolution (e.g., Rspack/Vite/webpack aliases) or selective inclusion.
|
|
32
32
|
- First-class loader (`@knighted/css/loader`) so bundlers can import compiled CSS alongside their modules via `?knighted-css`.
|
|
33
|
-
- Built-in type generation CLI (`knighted-css-generate-types`) that emits `.knighted-css.*` selector manifests so TypeScript
|
|
33
|
+
- Built-in type generation CLI (`knighted-css-generate-types`) that emits `.knighted-css.*` selector manifests (module mode) or declaration augmentations (declaration mode) so TypeScript stays in lockstep with loader exports.
|
|
34
34
|
|
|
35
35
|
## Requirements
|
|
36
36
|
|
|
@@ -107,7 +107,7 @@ See [docs/loader.md](../../docs/loader.md) for the full configuration, combined
|
|
|
107
107
|
|
|
108
108
|
### Type generation hook (`*.knighted-css*`)
|
|
109
109
|
|
|
110
|
-
Run `knighted-css-generate-types` so every specifier that ends with `.knighted-css` produces a sibling manifest containing literal selector tokens:
|
|
110
|
+
Run `knighted-css-generate-types` so every specifier that ends with `.knighted-css` produces a sibling manifest containing literal selector tokens (module mode, the default):
|
|
111
111
|
|
|
112
112
|
```ts
|
|
113
113
|
import stableSelectors from './button.module.scss.knighted-css.js'
|
|
@@ -137,6 +137,58 @@ selectors.card // hashed CSS Modules class name
|
|
|
137
137
|
> `--hashed` requires wiring `@knighted/css/loader-bridge` to handle `?knighted-css` queries so
|
|
138
138
|
> the generated proxies can read `knightedCss` and `knightedCssModules` at build time.
|
|
139
139
|
|
|
140
|
+
> [!NOTE]
|
|
141
|
+
> `--hashed` builds the selector list from compiled CSS. The generated sidecar can therefore
|
|
142
|
+
> include class names that are not exported by the module (e.g. sprinkles output), while the
|
|
143
|
+
> runtime `selectors` map only includes exported locals from the loader bridge.
|
|
144
|
+
|
|
145
|
+
Prefer module-level imports without the double extension? Use declaration mode to emit `.d.ts` augmentations next to JS/TS modules that import styles:
|
|
146
|
+
|
|
147
|
+
```sh
|
|
148
|
+
knighted-css-generate-types --root . --include src --mode declaration
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
```ts
|
|
152
|
+
import Button, { knightedCss, stableSelectors } from './button.js'
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
See [docs/type-generation.md](../../docs/type-generation.md#mode-quick-reference) for a quick comparison of module vs declaration mode tradeoffs.
|
|
156
|
+
|
|
157
|
+
> [!IMPORTANT]
|
|
158
|
+
> Declaration mode requires a resolver plugin to append `?knighted-css` (and `&combined` when applicable)
|
|
159
|
+
> at build time so runtime exports match the generated types.
|
|
160
|
+
|
|
161
|
+
Install the resolver plugin via `@knighted/css/plugin` and wire it into your bundler resolver:
|
|
162
|
+
|
|
163
|
+
```js
|
|
164
|
+
// rspack.config.js
|
|
165
|
+
import { knightedCssResolverPlugin } from '@knighted/css/plugin'
|
|
166
|
+
|
|
167
|
+
export default {
|
|
168
|
+
resolve: {
|
|
169
|
+
plugins: [knightedCssResolverPlugin()],
|
|
170
|
+
},
|
|
171
|
+
}
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
If you want the resolver to only match sidecars generated by the CLI, enable strict mode and provide a manifest (written by `knighted-css-generate-types --manifest`):
|
|
175
|
+
|
|
176
|
+
```js
|
|
177
|
+
import path from 'node:path'
|
|
178
|
+
import { knightedCssResolverPlugin } from '@knighted/css/plugin'
|
|
179
|
+
|
|
180
|
+
export default {
|
|
181
|
+
resolve: {
|
|
182
|
+
plugins: [
|
|
183
|
+
knightedCssResolverPlugin({
|
|
184
|
+
strictSidecar: true,
|
|
185
|
+
manifestPath: path.resolve('.knighted-css/knighted-manifest.json'),
|
|
186
|
+
}),
|
|
187
|
+
],
|
|
188
|
+
},
|
|
189
|
+
}
|
|
190
|
+
```
|
|
191
|
+
|
|
140
192
|
Refer to [docs/type-generation.md](../../docs/type-generation.md) for CLI options and workflow tips.
|
|
141
193
|
|
|
142
194
|
### Combined + runtime selectors
|
|
@@ -66,6 +66,7 @@ function getImportMetaUrl() {
|
|
|
66
66
|
}
|
|
67
67
|
const SELECTOR_REFERENCE = '.knighted-css';
|
|
68
68
|
const SELECTOR_MODULE_SUFFIX = '.knighted-css.ts';
|
|
69
|
+
const DECLARATION_SUFFIX = '.d.ts';
|
|
69
70
|
const STYLE_EXTENSIONS = css_js_1.DEFAULT_EXTENSIONS.map(ext => ext.toLowerCase());
|
|
70
71
|
const SCRIPT_EXTENSIONS = Array.from(SUPPORTED_EXTENSIONS);
|
|
71
72
|
const RESOLUTION_EXTENSIONS = Array.from(new Set([...SCRIPT_EXTENSIONS, ...STYLE_EXTENSIONS]));
|
|
@@ -75,11 +76,16 @@ const EXTENSION_FALLBACKS = {
|
|
|
75
76
|
'.cjs': ['.cts', '.cjs', '.js', '.ts', '.tsx'],
|
|
76
77
|
'.jsx': ['.tsx', '.jsx'],
|
|
77
78
|
};
|
|
79
|
+
const DECLARATION_MODE_WARNING = 'Declaration mode requires a resolver plugin to append ?knighted-css (and &combined when applicable) so runtime exports match the generated types.';
|
|
78
80
|
async function generateTypes(options = {}) {
|
|
79
81
|
const rootDir = await resolveRootDir(node_path_1.default.resolve(options.rootDir ?? process.cwd()));
|
|
80
82
|
const include = normalizeIncludeOptions(options.include, rootDir);
|
|
81
83
|
const cacheDir = node_path_1.default.resolve(options.outDir ?? node_path_1.default.join(rootDir, '.knighted-css'));
|
|
82
84
|
const tsconfig = loadTsconfigResolutionContext(rootDir);
|
|
85
|
+
const mode = options.mode ?? 'module';
|
|
86
|
+
const manifestPath = options.manifestPath
|
|
87
|
+
? node_path_1.default.resolve(rootDir, options.manifestPath)
|
|
88
|
+
: undefined;
|
|
83
89
|
await promises_1.default.mkdir(cacheDir, { recursive: true });
|
|
84
90
|
const internalOptions = {
|
|
85
91
|
rootDir,
|
|
@@ -90,6 +96,8 @@ async function generateTypes(options = {}) {
|
|
|
90
96
|
hashed: options.hashed,
|
|
91
97
|
tsconfig,
|
|
92
98
|
resolver: options.resolver,
|
|
99
|
+
mode,
|
|
100
|
+
manifestPath,
|
|
93
101
|
};
|
|
94
102
|
return generateDeclarations(internalOptions);
|
|
95
103
|
}
|
|
@@ -108,79 +116,176 @@ async function generateDeclarations(options) {
|
|
|
108
116
|
const selectorModulesManifestPath = node_path_1.default.join(options.cacheDir, 'selector-modules.json');
|
|
109
117
|
const previousSelectorManifest = await readManifest(selectorModulesManifestPath);
|
|
110
118
|
const nextSelectorManifest = {};
|
|
119
|
+
const sidecarManifest = {};
|
|
111
120
|
const selectorCache = new Map();
|
|
112
121
|
const processedSelectors = new Set();
|
|
113
122
|
const proxyInfoCache = new Map();
|
|
114
123
|
const warnings = [];
|
|
115
124
|
let selectorModuleWrites = 0;
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
const selectorSource = extractSelectorSourceSpecifier(resource);
|
|
123
|
-
if (!selectorSource) {
|
|
125
|
+
if (options.mode === 'declaration') {
|
|
126
|
+
warnings.push(DECLARATION_MODE_WARNING);
|
|
127
|
+
}
|
|
128
|
+
if (options.mode === 'declaration') {
|
|
129
|
+
for (const filePath of files) {
|
|
130
|
+
if (!isScriptResource(filePath)) {
|
|
124
131
|
continue;
|
|
125
132
|
}
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
133
|
+
if (!isWithinRoot(filePath, options.rootDir)) {
|
|
134
|
+
warnings.push(`Skipping declaration output for ${relativeToRoot(filePath, options.rootDir)} because it is outside the project root.`);
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
const manifestKey = buildSelectorModuleManifestKey(filePath);
|
|
138
|
+
if (processedSelectors.has(manifestKey)) {
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
const hasStyles = await hasStyleImports(filePath, {
|
|
142
|
+
rootDir: options.rootDir,
|
|
143
|
+
tsconfig: options.tsconfig,
|
|
144
|
+
resolver: options.resolver,
|
|
145
|
+
resolverFactory,
|
|
146
|
+
});
|
|
147
|
+
if (!hasStyles) {
|
|
148
|
+
processedSelectors.add(manifestKey);
|
|
130
149
|
continue;
|
|
131
150
|
}
|
|
132
|
-
const
|
|
151
|
+
const resolvedNamespace = (0, stableNamespace_js_1.resolveStableNamespace)(options.stableNamespace);
|
|
152
|
+
const cacheKey = `${filePath}::${resolvedNamespace}::declaration`;
|
|
133
153
|
let selectorMap = selectorCache.get(cacheKey);
|
|
134
154
|
if (!selectorMap) {
|
|
135
155
|
try {
|
|
136
|
-
|
|
137
|
-
const { css } = await activeCssWithMeta(resolvedPath, {
|
|
156
|
+
let cssResult = await activeCssWithMeta(filePath, {
|
|
138
157
|
cwd: options.rootDir,
|
|
139
158
|
peerResolver,
|
|
140
159
|
autoStable: options.autoStable ? { namespace: resolvedNamespace } : undefined,
|
|
141
|
-
lightningcss: options.autoStable && shouldUseCssModules
|
|
142
|
-
? { cssModules: true }
|
|
143
|
-
: undefined,
|
|
144
160
|
resolver: options.resolver,
|
|
145
161
|
});
|
|
162
|
+
if (cssResult.files.length === 0 || cssResult.css.trim().length === 0) {
|
|
163
|
+
processedSelectors.add(manifestKey);
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
if (options.autoStable &&
|
|
167
|
+
cssResult.files.some(file => isCssModuleResource(file))) {
|
|
168
|
+
cssResult = await activeCssWithMeta(filePath, {
|
|
169
|
+
cwd: options.rootDir,
|
|
170
|
+
peerResolver,
|
|
171
|
+
autoStable: options.autoStable
|
|
172
|
+
? { namespace: resolvedNamespace }
|
|
173
|
+
: undefined,
|
|
174
|
+
lightningcss: { cssModules: true },
|
|
175
|
+
resolver: options.resolver,
|
|
176
|
+
});
|
|
177
|
+
}
|
|
146
178
|
selectorMap = options.hashed
|
|
147
|
-
? collectSelectorTokensFromCss(css)
|
|
179
|
+
? collectSelectorTokensFromCss(cssResult.css)
|
|
148
180
|
: (0, stableSelectorsLiteral_js_1.buildStableSelectorsLiteral)({
|
|
149
|
-
css,
|
|
181
|
+
css: cssResult.css,
|
|
150
182
|
namespace: resolvedNamespace,
|
|
151
|
-
resourcePath:
|
|
183
|
+
resourcePath: filePath,
|
|
152
184
|
emitWarning: message => warnings.push(message),
|
|
153
185
|
}).selectorMap;
|
|
154
186
|
}
|
|
155
187
|
catch (error) {
|
|
156
|
-
warnings.push(`Failed to extract CSS for ${relativeToRoot(
|
|
188
|
+
warnings.push(`Failed to extract CSS for ${relativeToRoot(filePath, options.rootDir)}: ${formatErrorMessage(error)}`);
|
|
189
|
+
processedSelectors.add(manifestKey);
|
|
157
190
|
continue;
|
|
158
191
|
}
|
|
159
192
|
selectorCache.set(cacheKey, selectorMap);
|
|
160
193
|
}
|
|
161
|
-
if (!
|
|
162
|
-
|
|
194
|
+
if (!selectorMap || selectorMap.size === 0) {
|
|
195
|
+
processedSelectors.add(manifestKey);
|
|
163
196
|
continue;
|
|
164
197
|
}
|
|
165
|
-
const
|
|
166
|
-
if (
|
|
198
|
+
const proxyInfo = await resolveDeclarationProxyInfo(manifestKey, filePath, proxyInfoCache);
|
|
199
|
+
if (!proxyInfo) {
|
|
200
|
+
processedSelectors.add(manifestKey);
|
|
167
201
|
continue;
|
|
168
202
|
}
|
|
169
|
-
const
|
|
170
|
-
|
|
203
|
+
const moduleWrite = await ensureDeclarationModule(filePath, selectorMap, previousSelectorManifest, nextSelectorManifest, proxyInfo, options.hashed ?? false);
|
|
204
|
+
if (options.manifestPath) {
|
|
205
|
+
sidecarManifest[manifestKey] = { file: buildDeclarationPath(filePath) };
|
|
206
|
+
}
|
|
171
207
|
if (moduleWrite) {
|
|
172
208
|
selectorModuleWrites += 1;
|
|
173
209
|
}
|
|
174
210
|
processedSelectors.add(manifestKey);
|
|
175
211
|
}
|
|
176
212
|
}
|
|
213
|
+
else {
|
|
214
|
+
for (const filePath of files) {
|
|
215
|
+
const matches = await findSpecifierImports(filePath);
|
|
216
|
+
for (const match of matches) {
|
|
217
|
+
const cleaned = match.specifier.trim();
|
|
218
|
+
const inlineFree = stripInlineLoader(cleaned);
|
|
219
|
+
const { resource } = splitResourceAndQuery(inlineFree);
|
|
220
|
+
const selectorSource = extractSelectorSourceSpecifier(resource);
|
|
221
|
+
if (!selectorSource) {
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
const resolvedNamespace = (0, stableNamespace_js_1.resolveStableNamespace)(options.stableNamespace);
|
|
225
|
+
const resolvedPath = await resolveImportPath(selectorSource, match.importer, options.rootDir, options.tsconfig, options.resolver, resolverFactory, RESOLUTION_EXTENSIONS);
|
|
226
|
+
if (!resolvedPath) {
|
|
227
|
+
warnings.push(`Unable to resolve ${selectorSource} referenced by ${relativeToRoot(match.importer, options.rootDir)}.`);
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
const cacheKey = `${resolvedPath}::${resolvedNamespace}`;
|
|
231
|
+
let selectorMap = selectorCache.get(cacheKey);
|
|
232
|
+
if (!selectorMap) {
|
|
233
|
+
try {
|
|
234
|
+
const shouldUseCssModules = resolvedPath.endsWith('.module.css');
|
|
235
|
+
const { css } = await activeCssWithMeta(resolvedPath, {
|
|
236
|
+
cwd: options.rootDir,
|
|
237
|
+
peerResolver,
|
|
238
|
+
autoStable: options.autoStable
|
|
239
|
+
? { namespace: resolvedNamespace }
|
|
240
|
+
: undefined,
|
|
241
|
+
lightningcss: options.autoStable && shouldUseCssModules
|
|
242
|
+
? { cssModules: true }
|
|
243
|
+
: undefined,
|
|
244
|
+
resolver: options.resolver,
|
|
245
|
+
});
|
|
246
|
+
selectorMap = options.hashed
|
|
247
|
+
? collectSelectorTokensFromCss(css)
|
|
248
|
+
: (0, stableSelectorsLiteral_js_1.buildStableSelectorsLiteral)({
|
|
249
|
+
css,
|
|
250
|
+
namespace: resolvedNamespace,
|
|
251
|
+
resourcePath: resolvedPath,
|
|
252
|
+
emitWarning: message => warnings.push(message),
|
|
253
|
+
}).selectorMap;
|
|
254
|
+
}
|
|
255
|
+
catch (error) {
|
|
256
|
+
warnings.push(`Failed to extract CSS for ${relativeToRoot(resolvedPath, options.rootDir)}: ${formatErrorMessage(error)}`);
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
selectorCache.set(cacheKey, selectorMap);
|
|
260
|
+
}
|
|
261
|
+
if (!isWithinRoot(resolvedPath, options.rootDir)) {
|
|
262
|
+
warnings.push(`Skipping selector module for ${relativeToRoot(resolvedPath, options.rootDir)} because it is outside the project root.`);
|
|
263
|
+
continue;
|
|
264
|
+
}
|
|
265
|
+
const manifestKey = buildSelectorModuleManifestKey(resolvedPath);
|
|
266
|
+
if (processedSelectors.has(manifestKey)) {
|
|
267
|
+
continue;
|
|
268
|
+
}
|
|
269
|
+
const proxyInfo = await resolveProxyInfo(manifestKey, selectorSource, resolvedPath, proxyInfoCache);
|
|
270
|
+
const moduleWrite = await ensureSelectorModule(resolvedPath, selectorMap, previousSelectorManifest, nextSelectorManifest, selectorSource, proxyInfo ?? undefined, options.hashed ?? false);
|
|
271
|
+
if (moduleWrite) {
|
|
272
|
+
selectorModuleWrites += 1;
|
|
273
|
+
}
|
|
274
|
+
processedSelectors.add(manifestKey);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
177
278
|
const selectorModulesRemoved = await removeStaleSelectorModules(previousSelectorManifest, nextSelectorManifest);
|
|
178
279
|
await writeManifest(selectorModulesManifestPath, nextSelectorManifest);
|
|
280
|
+
if (options.manifestPath && options.mode === 'declaration') {
|
|
281
|
+
await writeSidecarManifest(options.manifestPath, sidecarManifest);
|
|
282
|
+
}
|
|
179
283
|
return {
|
|
180
284
|
selectorModulesWritten: selectorModuleWrites,
|
|
181
285
|
selectorModulesRemoved,
|
|
182
286
|
warnings,
|
|
183
287
|
manifestPath: selectorModulesManifestPath,
|
|
288
|
+
sidecarManifestPath: options.mode === 'declaration' ? options.manifestPath : undefined,
|
|
184
289
|
};
|
|
185
290
|
}
|
|
186
291
|
function normalizeIncludeOptions(include, rootDir) {
|
|
@@ -359,6 +464,50 @@ function buildSelectorModulePath(resolvedPath) {
|
|
|
359
464
|
const base = ext ? resolvedPath.slice(0, -ext.length) : resolvedPath;
|
|
360
465
|
return `${base}${SELECTOR_MODULE_SUFFIX}`;
|
|
361
466
|
}
|
|
467
|
+
function buildDeclarationModuleSpecifier(resolvedPath) {
|
|
468
|
+
const ext = node_path_1.default.extname(resolvedPath).toLowerCase();
|
|
469
|
+
const baseName = node_path_1.default.basename(resolvedPath, ext);
|
|
470
|
+
const mappedExt = ext === '.mjs' || ext === '.mts'
|
|
471
|
+
? '.mjs'
|
|
472
|
+
: ext === '.cjs' || ext === '.cts'
|
|
473
|
+
? '.cjs'
|
|
474
|
+
: '.js';
|
|
475
|
+
return `./${baseName}${mappedExt}`;
|
|
476
|
+
}
|
|
477
|
+
function buildDeclarationPath(resolvedPath) {
|
|
478
|
+
if (resolvedPath.endsWith(DECLARATION_SUFFIX)) {
|
|
479
|
+
return resolvedPath;
|
|
480
|
+
}
|
|
481
|
+
return `${resolvedPath}${DECLARATION_SUFFIX}`;
|
|
482
|
+
}
|
|
483
|
+
function formatSelectorTypeLiteral(selectors) {
|
|
484
|
+
const entries = Array.from(selectors.keys()).sort((a, b) => a.localeCompare(b));
|
|
485
|
+
const typeLines = entries.map(token => ` readonly ${JSON.stringify(token)}: string`);
|
|
486
|
+
return typeLines.length > 0
|
|
487
|
+
? `{
|
|
488
|
+
${typeLines.join('\n')}
|
|
489
|
+
}`
|
|
490
|
+
: 'Record<string, string>';
|
|
491
|
+
}
|
|
492
|
+
function formatDeclarationSource(selectors, proxyInfo, options = {}) {
|
|
493
|
+
const header = '// Generated by @knighted/css/generate-types\n// Do not edit.';
|
|
494
|
+
const isHashed = options.hashed === true;
|
|
495
|
+
const marker = isHashed ? '// @knighted-css:hashed' : '// @knighted-css';
|
|
496
|
+
const exportName = isHashed ? 'selectors' : 'stableSelectors';
|
|
497
|
+
const typeLiteral = formatSelectorTypeLiteral(selectors);
|
|
498
|
+
const shouldEmit = (name) => !proxyInfo.exportedNames?.has(name);
|
|
499
|
+
const lines = [
|
|
500
|
+
header,
|
|
501
|
+
marker,
|
|
502
|
+
'',
|
|
503
|
+
`declare module '${proxyInfo.moduleSpecifier}' {`,
|
|
504
|
+
shouldEmit('knightedCss') ? ' export const knightedCss: string' : '',
|
|
505
|
+
shouldEmit(exportName) ? ` export const ${exportName}: ${typeLiteral}` : '',
|
|
506
|
+
'}',
|
|
507
|
+
'export {}',
|
|
508
|
+
].filter(Boolean);
|
|
509
|
+
return `${lines.join('\n')}\n`;
|
|
510
|
+
}
|
|
362
511
|
function formatSelectorModuleSource(selectors, proxyInfo, options = {}) {
|
|
363
512
|
const header = '// Generated by @knighted/css/generate-types\n// Do not edit.';
|
|
364
513
|
const entries = Array.from(selectors.entries()).sort(([a], [b]) => a.localeCompare(b));
|
|
@@ -436,6 +585,10 @@ async function readManifest(manifestPath) {
|
|
|
436
585
|
async function writeManifest(manifestPath, manifest) {
|
|
437
586
|
await promises_1.default.writeFile(manifestPath, JSON.stringify(manifest, null, 2), 'utf8');
|
|
438
587
|
}
|
|
588
|
+
async function writeSidecarManifest(manifestPath, manifest) {
|
|
589
|
+
await promises_1.default.mkdir(node_path_1.default.dirname(manifestPath), { recursive: true });
|
|
590
|
+
await promises_1.default.writeFile(manifestPath, JSON.stringify(manifest, null, 2), 'utf8');
|
|
591
|
+
}
|
|
439
592
|
async function removeStaleSelectorModules(previous, next) {
|
|
440
593
|
const stale = Object.entries(previous).filter(([key]) => !next[key]);
|
|
441
594
|
let removed = 0;
|
|
@@ -480,6 +633,19 @@ async function ensureSelectorModule(resolvedPath, selectors, previousManifest, n
|
|
|
480
633
|
nextManifest[manifestKey] = { file: targetPath, hash };
|
|
481
634
|
return needsWrite;
|
|
482
635
|
}
|
|
636
|
+
async function ensureDeclarationModule(resolvedPath, selectors, previousManifest, nextManifest, proxyInfo, hashed) {
|
|
637
|
+
const manifestKey = buildSelectorModuleManifestKey(resolvedPath);
|
|
638
|
+
const targetPath = buildDeclarationPath(resolvedPath);
|
|
639
|
+
const source = formatDeclarationSource(selectors, proxyInfo, { hashed });
|
|
640
|
+
const hash = hashContent(source);
|
|
641
|
+
const previousEntry = previousManifest[manifestKey];
|
|
642
|
+
const needsWrite = previousEntry?.hash !== hash || !(await fileExists(targetPath));
|
|
643
|
+
if (needsWrite) {
|
|
644
|
+
await promises_1.default.writeFile(targetPath, source, 'utf8');
|
|
645
|
+
}
|
|
646
|
+
nextManifest[manifestKey] = { file: targetPath, hash };
|
|
647
|
+
return needsWrite;
|
|
648
|
+
}
|
|
483
649
|
async function fileExists(target) {
|
|
484
650
|
try {
|
|
485
651
|
await promises_1.default.access(target);
|
|
@@ -621,6 +787,60 @@ function isStyleResource(filePath) {
|
|
|
621
787
|
const normalized = filePath.toLowerCase();
|
|
622
788
|
return STYLE_EXTENSIONS.some(ext => normalized.endsWith(ext));
|
|
623
789
|
}
|
|
790
|
+
function isCssModuleResource(filePath) {
|
|
791
|
+
return /\.module\.(css|scss|sass|less)$/i.test(filePath);
|
|
792
|
+
}
|
|
793
|
+
function isScriptResource(filePath) {
|
|
794
|
+
const normalized = filePath.toLowerCase();
|
|
795
|
+
if (normalized.endsWith('.d.ts')) {
|
|
796
|
+
return false;
|
|
797
|
+
}
|
|
798
|
+
return SCRIPT_EXTENSIONS.some(ext => normalized.endsWith(ext));
|
|
799
|
+
}
|
|
800
|
+
async function hasStyleImports(filePath, options) {
|
|
801
|
+
let source;
|
|
802
|
+
try {
|
|
803
|
+
source = await promises_1.default.readFile(filePath, 'utf8');
|
|
804
|
+
}
|
|
805
|
+
catch {
|
|
806
|
+
return false;
|
|
807
|
+
}
|
|
808
|
+
const candidates = new Set();
|
|
809
|
+
try {
|
|
810
|
+
const analysis = await (0, lexer_js_1.analyzeModule)(source, filePath);
|
|
811
|
+
for (const specifier of analysis.imports) {
|
|
812
|
+
if (specifier) {
|
|
813
|
+
candidates.add(specifier);
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
catch {
|
|
818
|
+
// fall back to regex scanning below
|
|
819
|
+
}
|
|
820
|
+
const importRegex = /(import|require)\s*(?:\(|[^'"`]*)(['"])([^'"`]+)\2/g;
|
|
821
|
+
let match;
|
|
822
|
+
while ((match = importRegex.exec(source)) !== null) {
|
|
823
|
+
const specifier = match[3];
|
|
824
|
+
if (specifier) {
|
|
825
|
+
candidates.add(specifier);
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
for (const specifier of candidates) {
|
|
829
|
+
const cleaned = stripInlineLoader(specifier.trim());
|
|
830
|
+
const { resource } = splitResourceAndQuery(cleaned);
|
|
831
|
+
if (!resource) {
|
|
832
|
+
continue;
|
|
833
|
+
}
|
|
834
|
+
if (isStyleResource(resource)) {
|
|
835
|
+
return true;
|
|
836
|
+
}
|
|
837
|
+
const resolved = await resolveImportPath(resource, filePath, options.rootDir, options.tsconfig, options.resolver, options.resolverFactory, RESOLUTION_EXTENSIONS);
|
|
838
|
+
if (resolved && isStyleResource(resolved)) {
|
|
839
|
+
return true;
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
return false;
|
|
843
|
+
}
|
|
624
844
|
function collectSelectorTokensFromCss(css) {
|
|
625
845
|
const tokens = new Set();
|
|
626
846
|
const pattern = /\.([A-Za-z_-][A-Za-z0-9_-]*)\b/g;
|
|
@@ -653,6 +873,21 @@ async function resolveProxyInfo(manifestKey, selectorSource, resolvedPath, cache
|
|
|
653
873
|
cache.set(manifestKey, proxyInfo);
|
|
654
874
|
return proxyInfo;
|
|
655
875
|
}
|
|
876
|
+
async function resolveDeclarationProxyInfo(manifestKey, resolvedPath, cache) {
|
|
877
|
+
const cached = cache.get(manifestKey);
|
|
878
|
+
if (cached !== undefined) {
|
|
879
|
+
return cached;
|
|
880
|
+
}
|
|
881
|
+
const defaultSignal = await getDefaultExportSignal(resolvedPath);
|
|
882
|
+
const exportedNames = await getNamedExports(resolvedPath);
|
|
883
|
+
const proxyInfo = {
|
|
884
|
+
moduleSpecifier: buildDeclarationModuleSpecifier(resolvedPath),
|
|
885
|
+
includeDefault: defaultSignal === 'has-default',
|
|
886
|
+
exportedNames,
|
|
887
|
+
};
|
|
888
|
+
cache.set(manifestKey, proxyInfo);
|
|
889
|
+
return proxyInfo;
|
|
890
|
+
}
|
|
656
891
|
function buildProxyModuleSpecifier(resolvedPath, selectorSource) {
|
|
657
892
|
const resolvedExt = node_path_1.default.extname(resolvedPath);
|
|
658
893
|
const baseName = node_path_1.default.basename(resolvedPath, resolvedExt);
|
|
@@ -670,6 +905,16 @@ async function getDefaultExportSignal(filePath) {
|
|
|
670
905
|
return 'unknown';
|
|
671
906
|
}
|
|
672
907
|
}
|
|
908
|
+
async function getNamedExports(filePath) {
|
|
909
|
+
try {
|
|
910
|
+
const source = await promises_1.default.readFile(filePath, 'utf8');
|
|
911
|
+
const analysis = await (0, lexer_js_1.analyzeModule)(source, filePath);
|
|
912
|
+
return new Set(analysis.exports ?? []);
|
|
913
|
+
}
|
|
914
|
+
catch {
|
|
915
|
+
return new Set();
|
|
916
|
+
}
|
|
917
|
+
}
|
|
673
918
|
function createProjectPeerResolver(rootDir) {
|
|
674
919
|
const resolver = getProjectRequire(rootDir);
|
|
675
920
|
return async (name) => {
|
|
@@ -742,6 +987,8 @@ async function runGenerateTypesCli(argv = process.argv.slice(2)) {
|
|
|
742
987
|
autoStable: parsed.autoStable,
|
|
743
988
|
hashed: parsed.hashed,
|
|
744
989
|
resolver,
|
|
990
|
+
mode: parsed.mode,
|
|
991
|
+
manifestPath: parsed.manifestPath,
|
|
745
992
|
});
|
|
746
993
|
reportCliResult(result);
|
|
747
994
|
}
|
|
@@ -759,10 +1006,20 @@ function parseCliArgs(argv) {
|
|
|
759
1006
|
let autoStable = false;
|
|
760
1007
|
let hashed = false;
|
|
761
1008
|
let resolver;
|
|
1009
|
+
let mode = 'module';
|
|
1010
|
+
let manifestPath;
|
|
762
1011
|
for (let i = 0; i < argv.length; i += 1) {
|
|
763
1012
|
const arg = argv[i];
|
|
764
1013
|
if (arg === '--help' || arg === '-h') {
|
|
765
|
-
return {
|
|
1014
|
+
return {
|
|
1015
|
+
rootDir,
|
|
1016
|
+
include,
|
|
1017
|
+
outDir,
|
|
1018
|
+
stableNamespace,
|
|
1019
|
+
autoStable,
|
|
1020
|
+
mode,
|
|
1021
|
+
help: true,
|
|
1022
|
+
};
|
|
766
1023
|
}
|
|
767
1024
|
if (arg === '--auto-stable') {
|
|
768
1025
|
autoStable = true;
|
|
@@ -796,6 +1053,14 @@ function parseCliArgs(argv) {
|
|
|
796
1053
|
outDir = value;
|
|
797
1054
|
continue;
|
|
798
1055
|
}
|
|
1056
|
+
if (arg === '--manifest') {
|
|
1057
|
+
const value = argv[++i];
|
|
1058
|
+
if (!value) {
|
|
1059
|
+
throw new Error('Missing value for --manifest');
|
|
1060
|
+
}
|
|
1061
|
+
manifestPath = value;
|
|
1062
|
+
continue;
|
|
1063
|
+
}
|
|
799
1064
|
if (arg === '--stable-namespace') {
|
|
800
1065
|
const value = argv[++i];
|
|
801
1066
|
if (!value) {
|
|
@@ -812,6 +1077,17 @@ function parseCliArgs(argv) {
|
|
|
812
1077
|
resolver = value;
|
|
813
1078
|
continue;
|
|
814
1079
|
}
|
|
1080
|
+
if (arg === '--mode') {
|
|
1081
|
+
const value = argv[++i];
|
|
1082
|
+
if (!value) {
|
|
1083
|
+
throw new Error('Missing value for --mode');
|
|
1084
|
+
}
|
|
1085
|
+
if (value !== 'module' && value !== 'declaration') {
|
|
1086
|
+
throw new Error(`Unknown mode: ${value}`);
|
|
1087
|
+
}
|
|
1088
|
+
mode = value;
|
|
1089
|
+
continue;
|
|
1090
|
+
}
|
|
815
1091
|
if (arg.startsWith('-')) {
|
|
816
1092
|
throw new Error(`Unknown flag: ${arg}`);
|
|
817
1093
|
}
|
|
@@ -820,20 +1096,35 @@ function parseCliArgs(argv) {
|
|
|
820
1096
|
if (autoStable && hashed) {
|
|
821
1097
|
throw new Error('Cannot combine --auto-stable with --hashed');
|
|
822
1098
|
}
|
|
823
|
-
|
|
1099
|
+
if (manifestPath && mode !== 'declaration') {
|
|
1100
|
+
throw new Error('Cannot use --manifest unless --mode is declaration');
|
|
1101
|
+
}
|
|
1102
|
+
return {
|
|
1103
|
+
rootDir,
|
|
1104
|
+
include,
|
|
1105
|
+
outDir,
|
|
1106
|
+
stableNamespace,
|
|
1107
|
+
autoStable,
|
|
1108
|
+
hashed,
|
|
1109
|
+
resolver,
|
|
1110
|
+
mode,
|
|
1111
|
+
manifestPath,
|
|
1112
|
+
};
|
|
824
1113
|
}
|
|
825
1114
|
function printHelp() {
|
|
826
1115
|
console.log(`Usage: knighted-css-generate-types [options]
|
|
827
1116
|
|
|
828
1117
|
Options:
|
|
829
|
-
-r, --root <path>
|
|
830
|
-
-i, --include <path>
|
|
831
|
-
--out-dir <path>
|
|
832
|
-
--stable-namespace <name>
|
|
833
|
-
--auto-stable
|
|
834
|
-
--hashed
|
|
835
|
-
--resolver <path>
|
|
836
|
-
|
|
1118
|
+
-r, --root <path> Project root directory (default: cwd)
|
|
1119
|
+
-i, --include <path> Additional directories/files to scan (repeatable)
|
|
1120
|
+
--out-dir <path> Directory to store selector module manifest cache
|
|
1121
|
+
--stable-namespace <name> Stable namespace prefix for generated selector maps
|
|
1122
|
+
--auto-stable Enable autoStable when extracting CSS for selectors
|
|
1123
|
+
--hashed Emit selectors backed by loader-bridge hashed modules
|
|
1124
|
+
--resolver <path> Path or package name exporting a CssResolver
|
|
1125
|
+
--mode <module|declaration> Emit selector modules (module) or declaration files (declaration)
|
|
1126
|
+
--manifest <path> Write a sidecar manifest (declaration mode only)
|
|
1127
|
+
-h, --help Show this help message
|
|
837
1128
|
`);
|
|
838
1129
|
}
|
|
839
1130
|
function reportCliResult(result) {
|
|
@@ -844,6 +1135,9 @@ function reportCliResult(result) {
|
|
|
844
1135
|
console.log(`[knighted-css] Selector modules updated: wrote ${result.selectorModulesWritten}, removed ${result.selectorModulesRemoved}.`);
|
|
845
1136
|
}
|
|
846
1137
|
console.log(`[knighted-css] Manifest: ${result.manifestPath}`);
|
|
1138
|
+
if (result.sidecarManifestPath) {
|
|
1139
|
+
console.log(`[knighted-css] Sidecar manifest: ${result.sidecarManifestPath}`);
|
|
1140
|
+
}
|
|
847
1141
|
for (const warning of result.warnings) {
|
|
848
1142
|
console.warn(`[knighted-css] ${warning}`);
|
|
849
1143
|
}
|
|
@@ -873,23 +1167,31 @@ exports.__generateTypesInternals = {
|
|
|
873
1167
|
setImportMetaUrlProvider,
|
|
874
1168
|
isNonRelativeSpecifier,
|
|
875
1169
|
isStyleResource,
|
|
1170
|
+
isCssModuleResource,
|
|
876
1171
|
resolveProxyInfo,
|
|
1172
|
+
resolveDeclarationProxyInfo,
|
|
877
1173
|
resolveWithExtensionFallback,
|
|
878
1174
|
resolveIndexFallback,
|
|
879
1175
|
createProjectPeerResolver,
|
|
880
1176
|
getProjectRequire,
|
|
881
1177
|
loadTsconfigResolutionContext,
|
|
882
1178
|
resolveWithTsconfigPaths,
|
|
1179
|
+
hasStyleImports,
|
|
883
1180
|
loadResolverModule,
|
|
884
1181
|
parseCliArgs,
|
|
885
1182
|
printHelp,
|
|
886
1183
|
reportCliResult,
|
|
887
1184
|
buildSelectorModuleManifestKey,
|
|
888
1185
|
buildSelectorModulePath,
|
|
1186
|
+
buildDeclarationModuleSpecifier,
|
|
889
1187
|
formatSelectorModuleSource,
|
|
1188
|
+
buildDeclarationPath,
|
|
1189
|
+
formatDeclarationSource,
|
|
1190
|
+
ensureDeclarationModule,
|
|
890
1191
|
ensureSelectorModule,
|
|
891
1192
|
removeStaleSelectorModules,
|
|
892
1193
|
readManifest,
|
|
893
1194
|
writeManifest,
|
|
1195
|
+
writeSidecarManifest,
|
|
894
1196
|
};
|
|
895
1197
|
//# sourceMappingURL=generateTypes.cjs.map
|