@knighted/css 1.0.0-rc.7 → 1.0.0-rc.9
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/bin/generate-types.js +31 -0
- package/dist/cjs/css.cjs +73 -21
- package/dist/cjs/css.d.cts +5 -4
- package/dist/cjs/generateTypes.cjs +529 -0
- package/dist/cjs/generateTypes.d.cts +54 -0
- package/dist/cjs/loader.cjs +51 -27
- package/dist/cjs/loader.d.cts +1 -0
- package/dist/cjs/loaderInternals.cjs +38 -1
- package/dist/cjs/loaderInternals.d.cts +7 -0
- package/dist/cjs/moduleGraph.cjs +431 -0
- package/dist/cjs/moduleGraph.d.cts +15 -0
- package/dist/cjs/sassInternals.cjs +6 -3
- package/dist/cjs/sassInternals.d.cts +3 -4
- package/dist/cjs/stableNamespace.cjs +12 -0
- package/dist/cjs/stableNamespace.d.cts +3 -0
- package/dist/cjs/stableSelectorsLiteral.cjs +104 -0
- package/dist/cjs/stableSelectorsLiteral.d.cts +19 -0
- package/dist/cjs/types.cjs +2 -0
- package/dist/cjs/types.d.cts +4 -0
- package/dist/css.d.ts +5 -4
- package/dist/css.js +73 -21
- package/dist/generateTypes.d.ts +54 -0
- package/dist/generateTypes.js +521 -0
- package/dist/loader.d.ts +1 -0
- package/dist/loader.js +50 -26
- package/dist/loaderInternals.d.ts +7 -0
- package/dist/loaderInternals.js +33 -0
- package/dist/moduleGraph.d.ts +15 -0
- package/dist/moduleGraph.js +425 -0
- package/dist/sassInternals.d.ts +3 -4
- package/dist/sassInternals.js +6 -3
- package/dist/stableNamespace.d.ts +3 -0
- package/dist/stableNamespace.js +8 -0
- package/dist/stableSelectorsLiteral.d.ts +19 -0
- package/dist/stableSelectorsLiteral.js +98 -0
- package/dist/types.d.ts +4 -0
- package/dist/types.js +1 -0
- package/loader-queries.d.ts +28 -0
- package/package.json +23 -11
- package/types-stub/index.d.ts +5 -0
- package/types.d.ts +1 -0
|
@@ -0,0 +1,521 @@
|
|
|
1
|
+
import crypto from 'node:crypto';
|
|
2
|
+
import fs from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { createRequire } from 'node:module';
|
|
5
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
6
|
+
import { init, parse } from 'es-module-lexer';
|
|
7
|
+
import { moduleType } from 'node-module-type';
|
|
8
|
+
import { cssWithMeta } from './css.js';
|
|
9
|
+
import { determineSelectorVariant, hasQueryFlag, TYPES_QUERY_FLAG, } from './loaderInternals.js';
|
|
10
|
+
import { buildStableSelectorsLiteral } from './stableSelectorsLiteral.js';
|
|
11
|
+
import { resolveStableNamespace } from './stableNamespace.js';
|
|
12
|
+
const DEFAULT_SKIP_DIRS = new Set([
|
|
13
|
+
'node_modules',
|
|
14
|
+
'.git',
|
|
15
|
+
'dist',
|
|
16
|
+
'build',
|
|
17
|
+
'coverage',
|
|
18
|
+
'.knighted-css',
|
|
19
|
+
'.next',
|
|
20
|
+
'.nuxt',
|
|
21
|
+
'.svelte-kit',
|
|
22
|
+
'.output',
|
|
23
|
+
'tmp',
|
|
24
|
+
]);
|
|
25
|
+
const SUPPORTED_EXTENSIONS = new Set([
|
|
26
|
+
'.ts',
|
|
27
|
+
'.tsx',
|
|
28
|
+
'.js',
|
|
29
|
+
'.jsx',
|
|
30
|
+
'.mts',
|
|
31
|
+
'.cts',
|
|
32
|
+
'.mjs',
|
|
33
|
+
'.cjs',
|
|
34
|
+
]);
|
|
35
|
+
function resolvePackageRoot() {
|
|
36
|
+
const detectedType = moduleType();
|
|
37
|
+
if (detectedType === 'commonjs' && typeof __dirname === 'string') {
|
|
38
|
+
return path.resolve(__dirname, '..');
|
|
39
|
+
}
|
|
40
|
+
const moduleUrl = getImportMetaUrl();
|
|
41
|
+
if (moduleUrl) {
|
|
42
|
+
return path.resolve(path.dirname(fileURLToPath(moduleUrl)), '..');
|
|
43
|
+
}
|
|
44
|
+
return path.resolve(process.cwd(), 'node_modules', '@knighted', 'css');
|
|
45
|
+
}
|
|
46
|
+
function getImportMetaUrl() {
|
|
47
|
+
try {
|
|
48
|
+
return (0, eval)('import.meta.url');
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return undefined;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
const PACKAGE_ROOT = resolvePackageRoot();
|
|
55
|
+
const DEFAULT_TYPES_ROOT = path.join(PACKAGE_ROOT, 'types-stub');
|
|
56
|
+
const DEFAULT_OUT_DIR = path.join(PACKAGE_ROOT, 'node_modules', '.knighted-css');
|
|
57
|
+
export async function generateTypes(options = {}) {
|
|
58
|
+
const rootDir = path.resolve(options.rootDir ?? process.cwd());
|
|
59
|
+
const include = normalizeIncludeOptions(options.include, rootDir);
|
|
60
|
+
const outDir = path.resolve(options.outDir ?? DEFAULT_OUT_DIR);
|
|
61
|
+
const typesRoot = path.resolve(options.typesRoot ?? DEFAULT_TYPES_ROOT);
|
|
62
|
+
await init;
|
|
63
|
+
await fs.mkdir(outDir, { recursive: true });
|
|
64
|
+
await fs.mkdir(typesRoot, { recursive: true });
|
|
65
|
+
const internalOptions = {
|
|
66
|
+
rootDir,
|
|
67
|
+
include,
|
|
68
|
+
outDir,
|
|
69
|
+
typesRoot,
|
|
70
|
+
stableNamespace: options.stableNamespace,
|
|
71
|
+
};
|
|
72
|
+
return generateDeclarations(internalOptions);
|
|
73
|
+
}
|
|
74
|
+
async function generateDeclarations(options) {
|
|
75
|
+
const peerResolver = createProjectPeerResolver(options.rootDir);
|
|
76
|
+
const files = await collectCandidateFiles(options.include);
|
|
77
|
+
const manifestPath = path.join(options.outDir, 'manifest.json');
|
|
78
|
+
const previousManifest = await readManifest(manifestPath);
|
|
79
|
+
const nextManifest = {};
|
|
80
|
+
const selectorCache = new Map();
|
|
81
|
+
const processedSpecifiers = new Set();
|
|
82
|
+
const declarations = [];
|
|
83
|
+
const warnings = [];
|
|
84
|
+
let writes = 0;
|
|
85
|
+
for (const filePath of files) {
|
|
86
|
+
const matches = await findSpecifierImports(filePath);
|
|
87
|
+
for (const match of matches) {
|
|
88
|
+
const cleaned = match.specifier.trim();
|
|
89
|
+
const inlineFree = stripInlineLoader(cleaned);
|
|
90
|
+
if (!inlineFree.includes('?knighted-css'))
|
|
91
|
+
continue;
|
|
92
|
+
const { resource, query } = splitResourceAndQuery(inlineFree);
|
|
93
|
+
if (!query || !hasQueryFlag(query, TYPES_QUERY_FLAG)) {
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
if (processedSpecifiers.has(cleaned)) {
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
const resolvedNamespace = resolveStableNamespace(options.stableNamespace);
|
|
100
|
+
const resolvedPath = await resolveImportPath(resource, match.importer, options.rootDir);
|
|
101
|
+
if (!resolvedPath) {
|
|
102
|
+
warnings.push(`Unable to resolve ${resource} referenced by ${relativeToRoot(match.importer, options.rootDir)}.`);
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
const cacheKey = `${resolvedPath}::${resolvedNamespace}`;
|
|
106
|
+
let selectorMap = selectorCache.get(cacheKey);
|
|
107
|
+
if (!selectorMap) {
|
|
108
|
+
try {
|
|
109
|
+
const { css } = await cssWithMeta(resolvedPath, {
|
|
110
|
+
cwd: options.rootDir,
|
|
111
|
+
peerResolver,
|
|
112
|
+
});
|
|
113
|
+
selectorMap = buildStableSelectorsLiteral({
|
|
114
|
+
css,
|
|
115
|
+
namespace: resolvedNamespace,
|
|
116
|
+
resourcePath: resolvedPath,
|
|
117
|
+
emitWarning: message => warnings.push(message),
|
|
118
|
+
}).selectorMap;
|
|
119
|
+
}
|
|
120
|
+
catch (error) {
|
|
121
|
+
warnings.push(`Failed to extract CSS for ${relativeToRoot(resolvedPath, options.rootDir)}: ${formatErrorMessage(error)}`);
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
selectorCache.set(cacheKey, selectorMap);
|
|
125
|
+
}
|
|
126
|
+
const variant = determineSelectorVariant(query);
|
|
127
|
+
const declaration = formatModuleDeclaration(cleaned, variant, selectorMap);
|
|
128
|
+
const declarationHash = hashContent(declaration);
|
|
129
|
+
const fileName = buildDeclarationFileName(cleaned);
|
|
130
|
+
const targetPath = path.join(options.outDir, fileName);
|
|
131
|
+
const previousEntry = previousManifest[cleaned];
|
|
132
|
+
const needsWrite = previousEntry?.hash !== declarationHash || !(await fileExists(targetPath));
|
|
133
|
+
if (needsWrite) {
|
|
134
|
+
await fs.writeFile(targetPath, declaration, 'utf8');
|
|
135
|
+
writes += 1;
|
|
136
|
+
}
|
|
137
|
+
nextManifest[cleaned] = { file: fileName, hash: declarationHash };
|
|
138
|
+
if (needsWrite) {
|
|
139
|
+
declarations.push({ specifier: cleaned, filePath: targetPath });
|
|
140
|
+
}
|
|
141
|
+
processedSpecifiers.add(cleaned);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
const removed = await removeStaleDeclarations(previousManifest, nextManifest, options.outDir);
|
|
145
|
+
await writeManifest(manifestPath, nextManifest);
|
|
146
|
+
const typesIndexPath = path.join(options.typesRoot, 'index.d.ts');
|
|
147
|
+
await writeTypesIndex(typesIndexPath, nextManifest, options.outDir);
|
|
148
|
+
if (Object.keys(nextManifest).length === 0) {
|
|
149
|
+
declarations.length = 0;
|
|
150
|
+
}
|
|
151
|
+
return {
|
|
152
|
+
written: writes,
|
|
153
|
+
removed,
|
|
154
|
+
declarations,
|
|
155
|
+
warnings,
|
|
156
|
+
outDir: options.outDir,
|
|
157
|
+
typesIndexPath,
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
function normalizeIncludeOptions(include, rootDir) {
|
|
161
|
+
if (!include || include.length === 0) {
|
|
162
|
+
return [rootDir];
|
|
163
|
+
}
|
|
164
|
+
return include.map(entry => path.isAbsolute(entry) ? entry : path.resolve(rootDir, entry));
|
|
165
|
+
}
|
|
166
|
+
async function collectCandidateFiles(entries) {
|
|
167
|
+
const files = [];
|
|
168
|
+
const visited = new Set();
|
|
169
|
+
async function walk(entryPath) {
|
|
170
|
+
const resolved = path.resolve(entryPath);
|
|
171
|
+
if (visited.has(resolved)) {
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
visited.add(resolved);
|
|
175
|
+
let stat;
|
|
176
|
+
try {
|
|
177
|
+
stat = await fs.stat(resolved);
|
|
178
|
+
}
|
|
179
|
+
catch {
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
if (stat.isDirectory()) {
|
|
183
|
+
const base = path.basename(resolved);
|
|
184
|
+
if (DEFAULT_SKIP_DIRS.has(base)) {
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
const children = await fs.readdir(resolved, { withFileTypes: true });
|
|
188
|
+
for (const child of children) {
|
|
189
|
+
await walk(path.join(resolved, child.name));
|
|
190
|
+
}
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
if (!stat.isFile()) {
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
const ext = path.extname(resolved).toLowerCase();
|
|
197
|
+
if (!SUPPORTED_EXTENSIONS.has(ext)) {
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
files.push(resolved);
|
|
201
|
+
}
|
|
202
|
+
for (const entry of entries) {
|
|
203
|
+
await walk(entry);
|
|
204
|
+
}
|
|
205
|
+
return files;
|
|
206
|
+
}
|
|
207
|
+
async function findSpecifierImports(filePath) {
|
|
208
|
+
let source;
|
|
209
|
+
try {
|
|
210
|
+
source = await fs.readFile(filePath, 'utf8');
|
|
211
|
+
}
|
|
212
|
+
catch {
|
|
213
|
+
return [];
|
|
214
|
+
}
|
|
215
|
+
if (!source.includes('?knighted-css')) {
|
|
216
|
+
return [];
|
|
217
|
+
}
|
|
218
|
+
const matches = [];
|
|
219
|
+
const [imports] = parse(source, filePath);
|
|
220
|
+
for (const record of imports) {
|
|
221
|
+
const specifier = record.n ?? source.slice(record.s, record.e);
|
|
222
|
+
if (specifier && specifier.includes('?knighted-css')) {
|
|
223
|
+
matches.push({ specifier, importer: filePath });
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
const requireRegex = /require\((['"])([^'"`]+?\?knighted-css[^'"`]*)\1\)/g;
|
|
227
|
+
let reqMatch;
|
|
228
|
+
while ((reqMatch = requireRegex.exec(source)) !== null) {
|
|
229
|
+
const spec = reqMatch[2];
|
|
230
|
+
if (spec) {
|
|
231
|
+
matches.push({ specifier: spec, importer: filePath });
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
return matches;
|
|
235
|
+
}
|
|
236
|
+
function stripInlineLoader(specifier) {
|
|
237
|
+
const idx = specifier.lastIndexOf('!');
|
|
238
|
+
return idx >= 0 ? specifier.slice(idx + 1) : specifier;
|
|
239
|
+
}
|
|
240
|
+
function splitResourceAndQuery(specifier) {
|
|
241
|
+
const hashIndex = specifier.indexOf('#');
|
|
242
|
+
const trimmed = hashIndex >= 0 ? specifier.slice(0, hashIndex) : specifier;
|
|
243
|
+
const queryIndex = trimmed.indexOf('?');
|
|
244
|
+
if (queryIndex < 0) {
|
|
245
|
+
return { resource: trimmed, query: '' };
|
|
246
|
+
}
|
|
247
|
+
return { resource: trimmed.slice(0, queryIndex), query: trimmed.slice(queryIndex) };
|
|
248
|
+
}
|
|
249
|
+
const projectRequireCache = new Map();
|
|
250
|
+
async function resolveImportPath(resourceSpecifier, importerPath, rootDir) {
|
|
251
|
+
if (!resourceSpecifier)
|
|
252
|
+
return undefined;
|
|
253
|
+
if (resourceSpecifier.startsWith('.')) {
|
|
254
|
+
return path.resolve(path.dirname(importerPath), resourceSpecifier);
|
|
255
|
+
}
|
|
256
|
+
if (resourceSpecifier.startsWith('/')) {
|
|
257
|
+
return path.resolve(rootDir, resourceSpecifier.slice(1));
|
|
258
|
+
}
|
|
259
|
+
const requireFromRoot = getProjectRequire(rootDir);
|
|
260
|
+
try {
|
|
261
|
+
return requireFromRoot.resolve(resourceSpecifier);
|
|
262
|
+
}
|
|
263
|
+
catch {
|
|
264
|
+
return undefined;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
function buildDeclarationFileName(specifier) {
|
|
268
|
+
const digest = crypto.createHash('sha1').update(specifier).digest('hex').slice(0, 12);
|
|
269
|
+
return `knt-${digest}.d.ts`;
|
|
270
|
+
}
|
|
271
|
+
function formatModuleDeclaration(specifier, variant, selectors) {
|
|
272
|
+
const literalSpecifier = JSON.stringify(specifier);
|
|
273
|
+
const selectorType = formatSelectorType(selectors);
|
|
274
|
+
const header = `declare module ${literalSpecifier} {`;
|
|
275
|
+
const footer = '}';
|
|
276
|
+
if (variant === 'types') {
|
|
277
|
+
return `${header}
|
|
278
|
+
export const knightedCss: string
|
|
279
|
+
export const stableSelectors: ${selectorType}
|
|
280
|
+
${footer}
|
|
281
|
+
`;
|
|
282
|
+
}
|
|
283
|
+
const stableLine = ` export const stableSelectors: ${selectorType}`;
|
|
284
|
+
const shared = ` const combined: KnightedCssCombinedModule<Record<string, unknown>>
|
|
285
|
+
export const knightedCss: string
|
|
286
|
+
${stableLine}`;
|
|
287
|
+
if (variant === 'combined') {
|
|
288
|
+
return `${header}
|
|
289
|
+
${shared}
|
|
290
|
+
export default combined
|
|
291
|
+
${footer}
|
|
292
|
+
`;
|
|
293
|
+
}
|
|
294
|
+
return `${header}
|
|
295
|
+
${shared}
|
|
296
|
+
${footer}
|
|
297
|
+
`;
|
|
298
|
+
}
|
|
299
|
+
function formatSelectorType(selectors) {
|
|
300
|
+
if (selectors.size === 0) {
|
|
301
|
+
return 'Readonly<Record<string, string>>';
|
|
302
|
+
}
|
|
303
|
+
const entries = Array.from(selectors.entries()).sort(([a], [b]) => a.localeCompare(b));
|
|
304
|
+
const lines = entries.map(([token, selector]) => ` readonly ${JSON.stringify(token)}: ${JSON.stringify(selector)}`);
|
|
305
|
+
return `Readonly<{
|
|
306
|
+
${lines.join('\n')}
|
|
307
|
+
}>`;
|
|
308
|
+
}
|
|
309
|
+
function hashContent(content) {
|
|
310
|
+
return crypto.createHash('sha1').update(content).digest('hex');
|
|
311
|
+
}
|
|
312
|
+
async function readManifest(manifestPath) {
|
|
313
|
+
try {
|
|
314
|
+
const raw = await fs.readFile(manifestPath, 'utf8');
|
|
315
|
+
return JSON.parse(raw);
|
|
316
|
+
}
|
|
317
|
+
catch {
|
|
318
|
+
return {};
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
async function writeManifest(manifestPath, manifest) {
|
|
322
|
+
await fs.writeFile(manifestPath, JSON.stringify(manifest, null, 2), 'utf8');
|
|
323
|
+
}
|
|
324
|
+
async function removeStaleDeclarations(previous, next, outDir) {
|
|
325
|
+
const stale = Object.entries(previous).filter(([specifier]) => !next[specifier]);
|
|
326
|
+
let removed = 0;
|
|
327
|
+
for (const [, entry] of stale) {
|
|
328
|
+
const targetPath = path.join(outDir, entry.file);
|
|
329
|
+
try {
|
|
330
|
+
await fs.unlink(targetPath);
|
|
331
|
+
removed += 1;
|
|
332
|
+
}
|
|
333
|
+
catch {
|
|
334
|
+
// ignore
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
return removed;
|
|
338
|
+
}
|
|
339
|
+
async function writeTypesIndex(indexPath, manifest, outDir) {
|
|
340
|
+
const header = '// Generated by @knighted/css/generate-types\n// Do not edit.\n';
|
|
341
|
+
const references = Object.values(manifest)
|
|
342
|
+
.sort((a, b) => a.file.localeCompare(b.file))
|
|
343
|
+
.map(entry => {
|
|
344
|
+
const rel = path
|
|
345
|
+
.relative(path.dirname(indexPath), path.join(outDir, entry.file))
|
|
346
|
+
.split(path.sep)
|
|
347
|
+
.join('/');
|
|
348
|
+
return `/// <reference path="${rel}" />`;
|
|
349
|
+
});
|
|
350
|
+
const content = references.length > 0
|
|
351
|
+
? `${header}
|
|
352
|
+
${references.join('\n')}
|
|
353
|
+
`
|
|
354
|
+
: `${header}
|
|
355
|
+
`;
|
|
356
|
+
await fs.writeFile(indexPath, content, 'utf8');
|
|
357
|
+
}
|
|
358
|
+
function formatErrorMessage(error) {
|
|
359
|
+
if (error instanceof Error && typeof error.message === 'string') {
|
|
360
|
+
return error.message;
|
|
361
|
+
}
|
|
362
|
+
return String(error);
|
|
363
|
+
}
|
|
364
|
+
function relativeToRoot(filePath, rootDir) {
|
|
365
|
+
return path.relative(rootDir, filePath) || filePath;
|
|
366
|
+
}
|
|
367
|
+
async function fileExists(target) {
|
|
368
|
+
try {
|
|
369
|
+
await fs.access(target);
|
|
370
|
+
return true;
|
|
371
|
+
}
|
|
372
|
+
catch {
|
|
373
|
+
return false;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
function createProjectPeerResolver(rootDir) {
|
|
377
|
+
const resolver = getProjectRequire(rootDir);
|
|
378
|
+
return async (name) => {
|
|
379
|
+
const resolved = resolver.resolve(name);
|
|
380
|
+
return import(pathToFileURL(resolved).href);
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
function getProjectRequire(rootDir) {
|
|
384
|
+
const cached = projectRequireCache.get(rootDir);
|
|
385
|
+
if (cached) {
|
|
386
|
+
return cached;
|
|
387
|
+
}
|
|
388
|
+
const anchor = path.join(rootDir, 'package.json');
|
|
389
|
+
let loader;
|
|
390
|
+
try {
|
|
391
|
+
loader = createRequire(anchor);
|
|
392
|
+
}
|
|
393
|
+
catch {
|
|
394
|
+
loader = createRequire(path.join(process.cwd(), 'package.json'));
|
|
395
|
+
}
|
|
396
|
+
projectRequireCache.set(rootDir, loader);
|
|
397
|
+
return loader;
|
|
398
|
+
}
|
|
399
|
+
export async function runGenerateTypesCli(argv = process.argv.slice(2)) {
|
|
400
|
+
let parsed;
|
|
401
|
+
try {
|
|
402
|
+
parsed = parseCliArgs(argv);
|
|
403
|
+
}
|
|
404
|
+
catch (error) {
|
|
405
|
+
console.error(`[knighted-css] ${formatErrorMessage(error)}`);
|
|
406
|
+
process.exitCode = 1;
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
if (parsed.help) {
|
|
410
|
+
printHelp();
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
try {
|
|
414
|
+
const result = await generateTypes({
|
|
415
|
+
rootDir: parsed.rootDir,
|
|
416
|
+
include: parsed.include,
|
|
417
|
+
outDir: parsed.outDir,
|
|
418
|
+
typesRoot: parsed.typesRoot,
|
|
419
|
+
stableNamespace: parsed.stableNamespace,
|
|
420
|
+
});
|
|
421
|
+
reportCliResult(result);
|
|
422
|
+
}
|
|
423
|
+
catch (error) {
|
|
424
|
+
console.error('[knighted-css] generate-types failed.');
|
|
425
|
+
console.error(error);
|
|
426
|
+
process.exitCode = 1;
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
function parseCliArgs(argv) {
|
|
430
|
+
let rootDir = process.cwd();
|
|
431
|
+
const include = [];
|
|
432
|
+
let outDir;
|
|
433
|
+
let typesRoot;
|
|
434
|
+
let stableNamespace;
|
|
435
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
436
|
+
const arg = argv[i];
|
|
437
|
+
if (arg === '--help' || arg === '-h') {
|
|
438
|
+
return { rootDir, include, outDir, typesRoot, stableNamespace, help: true };
|
|
439
|
+
}
|
|
440
|
+
if (arg === '--root' || arg === '-r') {
|
|
441
|
+
const value = argv[++i];
|
|
442
|
+
if (!value) {
|
|
443
|
+
throw new Error('Missing value for --root');
|
|
444
|
+
}
|
|
445
|
+
rootDir = path.resolve(value);
|
|
446
|
+
continue;
|
|
447
|
+
}
|
|
448
|
+
if (arg === '--include' || arg === '-i') {
|
|
449
|
+
const value = argv[++i];
|
|
450
|
+
if (!value) {
|
|
451
|
+
throw new Error('Missing value for --include');
|
|
452
|
+
}
|
|
453
|
+
include.push(value);
|
|
454
|
+
continue;
|
|
455
|
+
}
|
|
456
|
+
if (arg === '--out-dir') {
|
|
457
|
+
const value = argv[++i];
|
|
458
|
+
if (!value) {
|
|
459
|
+
throw new Error('Missing value for --out-dir');
|
|
460
|
+
}
|
|
461
|
+
outDir = value;
|
|
462
|
+
continue;
|
|
463
|
+
}
|
|
464
|
+
if (arg === '--types-root') {
|
|
465
|
+
const value = argv[++i];
|
|
466
|
+
if (!value) {
|
|
467
|
+
throw new Error('Missing value for --types-root');
|
|
468
|
+
}
|
|
469
|
+
typesRoot = value;
|
|
470
|
+
continue;
|
|
471
|
+
}
|
|
472
|
+
if (arg === '--stable-namespace') {
|
|
473
|
+
const value = argv[++i];
|
|
474
|
+
if (!value) {
|
|
475
|
+
throw new Error('Missing value for --stable-namespace');
|
|
476
|
+
}
|
|
477
|
+
stableNamespace = value;
|
|
478
|
+
continue;
|
|
479
|
+
}
|
|
480
|
+
if (arg.startsWith('-')) {
|
|
481
|
+
throw new Error(`Unknown flag: ${arg}`);
|
|
482
|
+
}
|
|
483
|
+
include.push(arg);
|
|
484
|
+
}
|
|
485
|
+
return { rootDir, include, outDir, typesRoot, stableNamespace };
|
|
486
|
+
}
|
|
487
|
+
function printHelp() {
|
|
488
|
+
console.log(`Usage: knighted-css-generate-types [options]
|
|
489
|
+
|
|
490
|
+
Options:
|
|
491
|
+
-r, --root <path> Project root directory (default: cwd)
|
|
492
|
+
-i, --include <path> Additional directories/files to scan (repeatable)
|
|
493
|
+
--out-dir <path> Output directory for generated declarations
|
|
494
|
+
--types-root <path> Directory for generated @types entrypoint
|
|
495
|
+
--stable-namespace <name> Stable namespace prefix for generated selector maps
|
|
496
|
+
-h, --help Show this help message
|
|
497
|
+
`);
|
|
498
|
+
}
|
|
499
|
+
function reportCliResult(result) {
|
|
500
|
+
if (result.written === 0 && result.removed === 0) {
|
|
501
|
+
console.log('[knighted-css] No changes to ?knighted-css&types declarations (cache is up to date).');
|
|
502
|
+
}
|
|
503
|
+
else {
|
|
504
|
+
console.log(`[knighted-css] Updated ${result.written} declaration(s), removed ${result.removed}, output in ${result.outDir}.`);
|
|
505
|
+
}
|
|
506
|
+
console.log(`[knighted-css] Type references: ${result.typesIndexPath}`);
|
|
507
|
+
for (const warning of result.warnings) {
|
|
508
|
+
console.warn(`[knighted-css] ${warning}`);
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
export const __generateTypesInternals = {
|
|
512
|
+
stripInlineLoader,
|
|
513
|
+
splitResourceAndQuery,
|
|
514
|
+
buildDeclarationFileName,
|
|
515
|
+
formatModuleDeclaration,
|
|
516
|
+
formatSelectorType,
|
|
517
|
+
normalizeIncludeOptions,
|
|
518
|
+
parseCliArgs,
|
|
519
|
+
printHelp,
|
|
520
|
+
reportCliResult,
|
|
521
|
+
};
|
package/dist/loader.d.ts
CHANGED
|
@@ -8,6 +8,7 @@ export interface KnightedCssVanillaOptions {
|
|
|
8
8
|
}
|
|
9
9
|
export interface KnightedCssLoaderOptions extends CssOptions {
|
|
10
10
|
vanilla?: KnightedCssVanillaOptions;
|
|
11
|
+
stableNamespace?: string;
|
|
11
12
|
}
|
|
12
13
|
declare const loader: LoaderDefinitionFunction<KnightedCssLoaderOptions>;
|
|
13
14
|
export declare const pitch: PitchLoaderDefinitionFunction<KnightedCssLoaderOptions>;
|
package/dist/loader.js
CHANGED
|
@@ -1,11 +1,25 @@
|
|
|
1
1
|
import { cssWithMeta, compileVanillaModule } from './css.js';
|
|
2
2
|
import { detectModuleDefaultExport } from './moduleInfo.js';
|
|
3
|
-
import { buildSanitizedQuery,
|
|
3
|
+
import { buildSanitizedQuery, hasCombinedQuery, hasNamedOnlyQueryFlag, hasQueryFlag, shouldEmitCombinedDefault, shouldForwardDefaultExport, TYPES_QUERY_FLAG, } from './loaderInternals.js';
|
|
4
|
+
import { buildStableSelectorsLiteral } from './stableSelectorsLiteral.js';
|
|
5
|
+
import { resolveStableNamespace } from './stableNamespace.js';
|
|
4
6
|
const DEFAULT_EXPORT_NAME = 'knightedCss';
|
|
5
7
|
const loader = async function loader(source) {
|
|
6
|
-
const { cssOptions, vanillaOptions } = resolveLoaderOptions(this);
|
|
8
|
+
const { cssOptions, vanillaOptions, stableNamespace: optionNamespace, } = resolveLoaderOptions(this);
|
|
9
|
+
const resolvedNamespace = resolveStableNamespace(optionNamespace);
|
|
10
|
+
const typesRequested = hasQueryFlag(this.resourceQuery, TYPES_QUERY_FLAG);
|
|
7
11
|
const css = await extractCss(this, cssOptions);
|
|
8
|
-
const
|
|
12
|
+
const stableSelectorsLiteral = typesRequested
|
|
13
|
+
? buildStableSelectorsLiteral({
|
|
14
|
+
css,
|
|
15
|
+
namespace: resolvedNamespace,
|
|
16
|
+
resourcePath: this.resourcePath,
|
|
17
|
+
emitWarning: message => emitKnightedWarning(this, message),
|
|
18
|
+
})
|
|
19
|
+
: undefined;
|
|
20
|
+
const injection = buildInjection(css, {
|
|
21
|
+
stableSelectorsLiteral: stableSelectorsLiteral?.literal,
|
|
22
|
+
});
|
|
9
23
|
const isStyleModule = this.resourcePath.endsWith('.css.ts');
|
|
10
24
|
if (isStyleModule) {
|
|
11
25
|
const { source: compiledSource } = await compileVanillaModule(this.resourcePath, cssOptions.cwd ?? this.rootContext ?? process.cwd(), cssOptions.peerResolver);
|
|
@@ -44,7 +58,9 @@ export const pitch = function pitch() {
|
|
|
44
58
|
return;
|
|
45
59
|
}
|
|
46
60
|
const request = buildProxyRequest(this);
|
|
47
|
-
const { cssOptions } = resolveLoaderOptions(this);
|
|
61
|
+
const { cssOptions, stableNamespace: optionNamespace } = resolveLoaderOptions(this);
|
|
62
|
+
const typesRequested = hasQueryFlag(this.resourceQuery, TYPES_QUERY_FLAG);
|
|
63
|
+
const resolvedNamespace = resolveStableNamespace(optionNamespace);
|
|
48
64
|
const skipSyntheticDefault = hasNamedOnlyQueryFlag(this.resourceQuery);
|
|
49
65
|
const defaultSignalPromise = skipSyntheticDefault
|
|
50
66
|
? Promise.resolve('unknown')
|
|
@@ -55,14 +71,25 @@ export const pitch = function pitch() {
|
|
|
55
71
|
skipSyntheticDefault,
|
|
56
72
|
detection: defaultSignal,
|
|
57
73
|
});
|
|
58
|
-
|
|
74
|
+
const stableSelectorsLiteral = typesRequested
|
|
75
|
+
? buildStableSelectorsLiteral({
|
|
76
|
+
css,
|
|
77
|
+
namespace: resolvedNamespace,
|
|
78
|
+
resourcePath: this.resourcePath,
|
|
79
|
+
emitWarning: message => emitKnightedWarning(this, message),
|
|
80
|
+
})
|
|
81
|
+
: undefined;
|
|
82
|
+
return createCombinedModule(request, css, {
|
|
83
|
+
emitDefault,
|
|
84
|
+
stableSelectorsLiteral: stableSelectorsLiteral?.literal,
|
|
85
|
+
});
|
|
59
86
|
});
|
|
60
87
|
};
|
|
61
88
|
loader.pitch = pitch;
|
|
62
89
|
export default loader;
|
|
63
90
|
function resolveLoaderOptions(ctx) {
|
|
64
91
|
const rawOptions = (typeof ctx.getOptions === 'function' ? ctx.getOptions() : {});
|
|
65
|
-
const { vanilla, ...rest } = rawOptions;
|
|
92
|
+
const { vanilla, stableNamespace, ...rest } = rawOptions;
|
|
66
93
|
const cssOptions = {
|
|
67
94
|
...rest,
|
|
68
95
|
cwd: rest.cwd ?? ctx.rootContext ?? process.cwd(),
|
|
@@ -70,6 +97,7 @@ function resolveLoaderOptions(ctx) {
|
|
|
70
97
|
return {
|
|
71
98
|
cssOptions,
|
|
72
99
|
vanillaOptions: vanilla,
|
|
100
|
+
stableNamespace,
|
|
73
101
|
};
|
|
74
102
|
}
|
|
75
103
|
async function extractCss(ctx, options) {
|
|
@@ -83,25 +111,12 @@ async function extractCss(ctx, options) {
|
|
|
83
111
|
function toSourceString(source) {
|
|
84
112
|
return typeof source === 'string' ? source : source.toString('utf8');
|
|
85
113
|
}
|
|
86
|
-
function buildInjection(css) {
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
const trimmed = query.startsWith('?') ? query.slice(1) : query;
|
|
93
|
-
if (!trimmed)
|
|
94
|
-
return false;
|
|
95
|
-
return trimmed
|
|
96
|
-
.split('&')
|
|
97
|
-
.filter(Boolean)
|
|
98
|
-
.some(part => isQueryFlag(part, COMBINED_QUERY_FLAG));
|
|
99
|
-
}
|
|
100
|
-
function hasNamedOnlyQueryFlag(query) {
|
|
101
|
-
if (!query)
|
|
102
|
-
return false;
|
|
103
|
-
const entries = splitQuery(query);
|
|
104
|
-
return entries.some(part => NAMED_ONLY_QUERY_FLAGS.some(flag => isQueryFlag(part, flag)));
|
|
114
|
+
function buildInjection(css, extras) {
|
|
115
|
+
const lines = [`\n\nexport const ${DEFAULT_EXPORT_NAME} = ${JSON.stringify(css)};\n`];
|
|
116
|
+
if (extras?.stableSelectorsLiteral) {
|
|
117
|
+
lines.push(extras.stableSelectorsLiteral);
|
|
118
|
+
}
|
|
119
|
+
return lines.join('');
|
|
105
120
|
}
|
|
106
121
|
function buildProxyRequest(ctx) {
|
|
107
122
|
const sanitizedQuery = buildSanitizedQuery(ctx.resourceQuery);
|
|
@@ -142,6 +157,15 @@ typeof __knightedModule.default !== 'undefined'
|
|
|
142
157
|
? __knightedModule.default
|
|
143
158
|
: __knightedModule;`, 'export default __knightedDefault;');
|
|
144
159
|
}
|
|
145
|
-
lines.push(buildInjection(css));
|
|
160
|
+
lines.push(buildInjection(css, { stableSelectorsLiteral: options?.stableSelectorsLiteral }));
|
|
146
161
|
return lines.join('\n');
|
|
147
162
|
}
|
|
163
|
+
function emitKnightedWarning(ctx, message) {
|
|
164
|
+
const formatted = `\x1b[33m@knighted/css warning\x1b[0m ${message}`;
|
|
165
|
+
if (typeof ctx.emitWarning === 'function') {
|
|
166
|
+
ctx.emitWarning(new Error(formatted));
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
// eslint-disable-next-line no-console
|
|
170
|
+
console.warn(formatted);
|
|
171
|
+
}
|
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
import type { ModuleDefaultSignal } from './moduleInfo.js';
|
|
2
2
|
export declare const COMBINED_QUERY_FLAG = "combined";
|
|
3
|
+
export declare const TYPES_QUERY_FLAG = "types";
|
|
3
4
|
export declare const NAMED_ONLY_QUERY_FLAGS: readonly ["named-only", "no-default"];
|
|
5
|
+
export type SelectorTypeVariant = 'types' | 'combined' | 'combinedWithoutDefault';
|
|
4
6
|
export declare function splitQuery(query: string): string[];
|
|
5
7
|
export declare function isQueryFlag(entry: string, flag: string): boolean;
|
|
6
8
|
export declare function buildSanitizedQuery(query?: string | null): string;
|
|
9
|
+
export declare function hasQueryFlag(query: string | null | undefined, flag: string): boolean;
|
|
7
10
|
export declare function shouldForwardDefaultExport(request: string): boolean;
|
|
11
|
+
export declare function hasCombinedQuery(query?: string | null): boolean;
|
|
12
|
+
export declare function hasNamedOnlyQueryFlag(query?: string | null): boolean;
|
|
13
|
+
export declare function determineSelectorVariant(query?: string | null): SelectorTypeVariant;
|
|
8
14
|
export declare function shouldEmitCombinedDefault(options: {
|
|
9
15
|
detection: ModuleDefaultSignal;
|
|
10
16
|
request: string;
|
|
@@ -13,4 +19,5 @@ export declare function shouldEmitCombinedDefault(options: {
|
|
|
13
19
|
export declare const __loaderInternals: {
|
|
14
20
|
buildSanitizedQuery: typeof buildSanitizedQuery;
|
|
15
21
|
shouldEmitCombinedDefault: typeof shouldEmitCombinedDefault;
|
|
22
|
+
determineSelectorVariant: typeof determineSelectorVariant;
|
|
16
23
|
};
|