synthesisui 0.16.467 → 0.16.469
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/commands/component.js +8 -1
- package/dist/commands/doctor.js +51 -2
- package/dist/commands/generate.js +7 -4
- package/dist/commands/import.js +41 -8
- package/dist/commands/refit.js +7 -4
- package/dist/commands/upgrade.js +19 -5
- package/dist/component-codegen.js +13 -1
- package/dist/doctor/architecture.js +30 -3
- package/dist/doctor/css-modules.js +45 -8
- package/dist/doctor/scan.js +32 -0
- package/dist/doctor/scss-lens.js +82 -0
- package/dist/doctor/template-class-variants.js +28 -0
- package/dist/doctor/ts-tokens-lens.js +137 -0
- package/dist/in-his-form.js +133 -0
- package/dist/install-marks.js +20 -2
- package/dist/project-facts.js +3 -2
- package/dist/scss-speech.js +61 -0
- package/dist/styles-flavour.js +10 -1
- package/dist/their-vars.js +2 -1
- package/package.json +1 -1
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
|
+
import { speakInHisProject } from "../in-his-form.js";
|
|
2
3
|
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
3
4
|
import { join } from "node:path";
|
|
4
5
|
import { emitCn, readVocabulary } from "../cn-codegen.js";
|
|
@@ -413,12 +414,18 @@ export async function component(slug, name, opts) {
|
|
|
413
414
|
*/
|
|
414
415
|
const nota = unreadComment(unreadForComponent(await readCensus(root), res.name));
|
|
415
416
|
/** Os bytes finais, para o fingerprint lembrar EXATAMENTE o que foi a disco (T7). */
|
|
416
|
-
const
|
|
417
|
+
const drafted = files.map((file) => ({
|
|
417
418
|
filename: file.filename,
|
|
418
419
|
content: file.filename.endsWith(".tsx")
|
|
419
420
|
? `${nota}${file.code}`
|
|
420
421
|
: file.code,
|
|
421
422
|
}));
|
|
423
|
+
/** Num projeto SCSS, a folha sai em SCSS e na grafia dele - ver `scss-speech.ts` (27/09). */
|
|
424
|
+
/**
|
|
425
|
+
* NA GRAFIA DELE, E DEPOIS NA FORMA DELE - `scss-speech.ts` troca `var(--x)` pelo `$x` dele, e
|
|
426
|
+
* `in-his-form.ts` troca o nosso `ds-` e a folha global pelo formato que o projeto usa (28/09).
|
|
427
|
+
*/
|
|
428
|
+
const written = await speakInHisProject(root, compDir, drafted, flavour, local, res.name);
|
|
422
429
|
for (const f of written)
|
|
423
430
|
await writeFile(join(compDir, f.filename), f.content, "utf8");
|
|
424
431
|
await recordWritten(join(root, "_synthesisui", "ds", slug), local, written);
|
package/dist/commands/doctor.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { mkdir, readdir, readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import { readStyleSheet, scssVariables } from "../doctor/scss-lens.js";
|
|
3
|
+
import { tsTokenObjects, tsTokensAsVars } from "../doctor/ts-tokens-lens.js";
|
|
2
4
|
import { dirname, join, relative, resolve } from "node:path";
|
|
3
5
|
import { describeIntent, intentOf, readProjectConfig } from "../config.js";
|
|
4
6
|
import { describeFix, planFix, readerFor, writeFix, } from "../doctor/apply-fix.js";
|
|
@@ -431,17 +433,45 @@ measured) {
|
|
|
431
433
|
for await (const file of walkAll(roots)) {
|
|
432
434
|
if (!/\.(css|scss|sass|less)$/i.test(file))
|
|
433
435
|
continue;
|
|
436
|
+
/**
|
|
437
|
+
* CRU, E NÃO PELA LENTE - a tabela é o que o doctor oferece como TROCA. Pela lente, `$gray_dark` viraria um
|
|
438
|
+
* `--gray_dark` que o navegador não conhece, e um `#555` num `.tsx` sairia com a sugestão de
|
|
439
|
+
* `var(--gray_dark)` - código que não pinta. O nome Sass continua dito, nunca escrito, por
|
|
440
|
+
* `aliases` (`their-names.ts`); a lente entra só na VARREDURA, onde conta o uso para a cobertura.
|
|
441
|
+
*/
|
|
434
442
|
const source = await readFile(file, "utf8").catch(() => "");
|
|
435
443
|
sheets.set(file, source);
|
|
436
444
|
css += `\n${source}`;
|
|
437
445
|
}
|
|
438
|
-
|
|
446
|
+
const table = buildTable({
|
|
439
447
|
css,
|
|
440
448
|
source: "yours",
|
|
441
449
|
sheets,
|
|
442
450
|
rootPx: measured?.px,
|
|
443
451
|
rootFrom: measured?.from ?? null,
|
|
444
452
|
});
|
|
453
|
+
/** As `$variáveis` Sass, à parte da tabela de trocas - ver `TokenTable.sassNames` (27/09). */
|
|
454
|
+
const sassNames = new Map();
|
|
455
|
+
for (const [file, source] of sheets)
|
|
456
|
+
if (/\.(scss|sass)$/i.test(file))
|
|
457
|
+
for (const [n, v] of scssVariables(source)) {
|
|
458
|
+
const key = `--${n.slice(1).toLowerCase()}`;
|
|
459
|
+
if (!sassNames.has(key))
|
|
460
|
+
sassNames.set(key, v);
|
|
461
|
+
}
|
|
462
|
+
if (sassNames.size > 0)
|
|
463
|
+
table.sassNames = sassNames;
|
|
464
|
+
/** E o objeto de tokens em TypeScript - ver `TokenTable.tsNames` (27/09). */
|
|
465
|
+
const tsNames = new Map();
|
|
466
|
+
for await (const file of walkAll(roots))
|
|
467
|
+
if (/\.(ts|tsx)$/.test(file) && !/\.d\.ts$/.test(file))
|
|
468
|
+
for (const o of tsTokenObjects(await readFile(file, "utf8").catch(() => "")))
|
|
469
|
+
for (const [n, v] of o.values)
|
|
470
|
+
if (!tsNames.has(n))
|
|
471
|
+
tsNames.set(n, v);
|
|
472
|
+
if (tsNames.size > 0)
|
|
473
|
+
table.tsNames = tsNames;
|
|
474
|
+
return table;
|
|
445
475
|
}
|
|
446
476
|
const KIND_LABEL = {
|
|
447
477
|
color: "colour",
|
|
@@ -695,6 +725,15 @@ export async function doctor(opts) {
|
|
|
695
725
|
*/
|
|
696
726
|
table = withTheirNames(installed.theirs, installed.theirs);
|
|
697
727
|
}
|
|
728
|
+
/**
|
|
729
|
+
* AS `$VARIÁVEIS` DELE VALEM COM OU SEM SISTEMA NOSSO - ver `TokenTable.sassNames` (27/09). Elas moram na
|
|
730
|
+
* tabela DELE (`installed.theirs`), e com um sistema instalado a varredura usa a nossa: sem este repasse, um
|
|
731
|
+
* `$accent` num `.scss` não entrava em conta nenhuma.
|
|
732
|
+
*/
|
|
733
|
+
if (installed.theirs.sassNames && !table.sassNames)
|
|
734
|
+
table = { ...table, sassNames: installed.theirs.sassNames };
|
|
735
|
+
if (installed.theirs.tsNames && !table.tsNames)
|
|
736
|
+
table = { ...table, tsNames: installed.theirs.tsNames };
|
|
698
737
|
const hasSystem = table.byName.size > 0;
|
|
699
738
|
/**
|
|
700
739
|
* O NOME DELE, ONDE ANTES ESTAVA "the system".
|
|
@@ -724,12 +763,22 @@ export async function doctor(opts) {
|
|
|
724
763
|
* Nobody debugs from 0%. They conclude the product does not work.
|
|
725
764
|
*/
|
|
726
765
|
const skippedProjects = [];
|
|
766
|
+
/** Os objetos de tokens em TypeScript, para a lente da varredura - lidos da RAIZ, como o vocabulário. */
|
|
767
|
+
const tsObjects = [];
|
|
768
|
+
for await (const file of walkAll([root]))
|
|
769
|
+
if (/\.(ts|tsx)$/.test(file) && !/\.d\.ts$/.test(file))
|
|
770
|
+
tsObjects.push(...tsTokenObjects(await readFile(file, "utf8").catch(() => "")));
|
|
727
771
|
const tally = emptyTally();
|
|
728
772
|
const internalSpecs = await internalSpecifiers(root);
|
|
729
773
|
for await (const file of scopes.length > 0
|
|
730
774
|
? walkAll(scopes)
|
|
731
775
|
: walk(root, skippedProjects)) {
|
|
732
|
-
|
|
776
|
+
/** Um `.scss` passa pela lente, ou `$accent` não conta como nome - ver `scss-lens.ts` (27/09). */
|
|
777
|
+
const src = /\.(scss|sass)$/i.test(file)
|
|
778
|
+
? await readStyleSheet(file)
|
|
779
|
+
: tsObjects.length > 0 && /\.(tsx|jsx|ts)$/.test(file)
|
|
780
|
+
? tsTokensAsVars(await readFile(file, "utf8").catch(() => ""), tsObjects)
|
|
781
|
+
: await readFile(file, "utf8").catch(() => "");
|
|
733
782
|
if (!src)
|
|
734
783
|
continue;
|
|
735
784
|
const rel = relative(root, file);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import { speakInHisProject } from "../in-his-form.js";
|
|
2
3
|
import { join } from "node:path";
|
|
3
4
|
import { generateComponentFiles } from "../component-codegen.js";
|
|
4
5
|
import { readProjectConfig, resolveRegistry } from "../config.js";
|
|
@@ -104,13 +105,15 @@ export async function generate(description, opts) {
|
|
|
104
105
|
console.log(` The recipe and the compiled CSS are on disk either way - only the .tsx was not written.`);
|
|
105
106
|
return;
|
|
106
107
|
}
|
|
107
|
-
|
|
108
|
-
|
|
108
|
+
/** Na grafia e na forma dele - ver `speakInHisProject` (28/09). */
|
|
109
|
+
const spoken = await speakInHisProject(root, compDir, files.map((x) => ({ filename: x.filename, content: x.code })), flavourOf(res.name), res.name, res.name);
|
|
110
|
+
for (const file of spoken) {
|
|
111
|
+
await writeFile(join(compDir, file.filename), file.content, "utf8");
|
|
109
112
|
}
|
|
110
113
|
/** O fingerprint do que escrevemos - protege a edição dele no upgrade (T7). */
|
|
111
|
-
await recordWritten(join(root, "_synthesisui", "ds", slug), res.name,
|
|
114
|
+
await recordWritten(join(root, "_synthesisui", "ds", slug), res.name, spoken);
|
|
112
115
|
materialized = true;
|
|
113
|
-
console.log(`✓ ${config.componentsDir}/${res.name}/ → ${
|
|
116
|
+
console.log(`✓ ${config.componentsDir}/${res.name}/ → ${spoken.map((f) => f.filename).join(", ")} (styles: ${config.styles})`);
|
|
114
117
|
}
|
|
115
118
|
console.log("");
|
|
116
119
|
console.log("Use it:");
|
package/dist/commands/import.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { mkdir, readdir, readFile, stat, writeFile } from "node:fs/promises";
|
|
2
|
+
import { readStyleSheet } from "../doctor/scss-lens.js";
|
|
3
|
+
import { tsTokenObjects, tsTokensAsVars } from "../doctor/ts-tokens-lens.js";
|
|
2
4
|
import { basename, dirname, join, relative, resolve, sep } from "node:path";
|
|
3
5
|
import { anatomyFromSketch } from "../anatomy-from-sketch.js";
|
|
4
6
|
import { applyAnatomyPatch, hasEdits, } from "../anatomy-patch.js";
|
|
@@ -45,6 +47,7 @@ import { variantCallsIn } from "../doctor/variant-calls.js";
|
|
|
45
47
|
import { transcribeVariants } from "../doctor/variant-read.js";
|
|
46
48
|
import { frontierKind, packageRoot } from "../frontier-kind.js";
|
|
47
49
|
import { keyframesInSheets } from "../global-keyframes.js";
|
|
50
|
+
import { templateClassVariants } from "../doctor/template-class-variants.js";
|
|
48
51
|
import { globalClassesWorn, porNecessidade } from "../global-wear.js";
|
|
49
52
|
import { withLibraryStructure } from "../library-structure.js";
|
|
50
53
|
import { architectureGaps, mergeCensus } from "../merge-census.js";
|
|
@@ -189,7 +192,7 @@ async function sheetWithImports(path, seen = new Set(), depth = 0) {
|
|
|
189
192
|
if (depth > 6 || seen.has(path))
|
|
190
193
|
return { css: "", files: [] };
|
|
191
194
|
seen.add(path);
|
|
192
|
-
const body = await
|
|
195
|
+
const body = await readStyleSheet(path);
|
|
193
196
|
if (!body)
|
|
194
197
|
return { css: "", files: [] };
|
|
195
198
|
let css = "";
|
|
@@ -275,7 +278,7 @@ alsoGlobalsFrom) {
|
|
|
275
278
|
for await (const file of walkAll([root])) {
|
|
276
279
|
if (!/\.(css|scss|sass|less)$/i.test(file))
|
|
277
280
|
continue;
|
|
278
|
-
const body = await
|
|
281
|
+
const body = await readStyleSheet(file);
|
|
279
282
|
sheets.push({ file, body });
|
|
280
283
|
}
|
|
281
284
|
/**
|
|
@@ -291,7 +294,7 @@ alsoGlobalsFrom) {
|
|
|
291
294
|
continue;
|
|
292
295
|
if (file === root || file.startsWith(dentro))
|
|
293
296
|
continue;
|
|
294
|
-
const body = await
|
|
297
|
+
const body = await readStyleSheet(file);
|
|
295
298
|
deForaDoEscopo.add(file);
|
|
296
299
|
sheets.push({ file, body });
|
|
297
300
|
}
|
|
@@ -552,7 +555,20 @@ export async function takeCensus(root, opts) {
|
|
|
552
555
|
/** Todo arquivo de estilo do escopo, para o ledger - ver o parâmetro `into`. */
|
|
553
556
|
const styleFiles = [];
|
|
554
557
|
const { css, globals: globalSheets } = await harvestOwnCss(root, styleFiles, repoRoot);
|
|
555
|
-
|
|
558
|
+
/**
|
|
559
|
+
* O OBJETO DE TOKENS EM TYPESCRIPT ENTRA NO VOCABULÁRIO - ver `ts-tokens-lens.ts` (rodada 9, 27/09). O
|
|
560
|
+
* teste-import-atomico declarava cor, papel, espaço, raio e fonte em `ui/theme/tokens.ts`, e o censo saía com
|
|
561
|
+
* `declared: {}`. O objeto é lido como `--caminho: valor`, e os componentes que o usam por `style` são lidos
|
|
562
|
+
* pela mesma lente mais abaixo.
|
|
563
|
+
*/
|
|
564
|
+
const tsObjects = [];
|
|
565
|
+
for await (const file of walkAll([root]))
|
|
566
|
+
if (/\.(ts|tsx)$/.test(file) && !/\.d\.ts$/.test(file))
|
|
567
|
+
tsObjects.push(...tsTokenObjects(await readFile(file, "utf8").catch(() => "")));
|
|
568
|
+
const tsRoot = tsObjects
|
|
569
|
+
.flatMap((o) => [...o.values].map(([n, v]) => `${n}: ${v};`))
|
|
570
|
+
.join(" ");
|
|
571
|
+
const table = buildTable({ css: tsRoot ? `${css}\n:root { ${tsRoot} }` : css, source: "yours" });
|
|
556
572
|
const schemes = parseSchemeBlocks(css);
|
|
557
573
|
const reports = [];
|
|
558
574
|
const tally = emptyTally();
|
|
@@ -750,7 +766,9 @@ export async function takeCensus(root, opts) {
|
|
|
750
766
|
*/
|
|
751
767
|
const progress = startProgress("Reading");
|
|
752
768
|
for await (const file of walk(root)) {
|
|
753
|
-
const
|
|
769
|
+
const raw = await readFile(file, "utf8").catch(() => "");
|
|
770
|
+
/** Um `style` que lê o objeto de tokens é lido como variável CSS - ver `ts-tokens-lens.ts` (27/09). */
|
|
771
|
+
const src = tsObjects.length > 0 && /\.(tsx|jsx|ts)$/.test(file) ? tsTokensAsVars(raw, tsObjects) : raw;
|
|
754
772
|
if (!src)
|
|
755
773
|
continue;
|
|
756
774
|
const rel = relative(root, file);
|
|
@@ -1192,7 +1210,11 @@ export async function takeCensus(root, opts) {
|
|
|
1192
1210
|
...fromModule?.dark,
|
|
1193
1211
|
...t.dark,
|
|
1194
1212
|
};
|
|
1195
|
-
|
|
1213
|
+
/** A classe global montada com texto (`btn--${variant}`) - ver `template-class-variants.ts` (27/09). */
|
|
1214
|
+
const fromTemplate = templateClassVariants(src, globalClasses);
|
|
1215
|
+
for (const cls of fromTemplate.claimed)
|
|
1216
|
+
globalClassesClaimed.add(cls);
|
|
1217
|
+
const layers = [...(fromModule?.layers ?? []), ...v.layers, ...fromTemplate.layers];
|
|
1196
1218
|
const parts = { ...fromModule?.parts, ...t.parts };
|
|
1197
1219
|
const tree = fromModule?.tree ?? [];
|
|
1198
1220
|
const size = Object.keys(base).length +
|
|
@@ -1488,8 +1510,19 @@ export async function takeCensus(root, opts) {
|
|
|
1488
1510
|
* `app/` and no `pages/`, so there is nothing to count and the count would be zero for
|
|
1489
1511
|
* everything. An app has them, and then the number means something.
|
|
1490
1512
|
*/
|
|
1491
|
-
const
|
|
1492
|
-
|
|
1513
|
+
const appScreens = new Set([...screensOf.values()].flatMap((at) => [...at].filter((s) => s.startsWith("app/") || s.startsWith("pages/"))));
|
|
1514
|
+
const hasScreens = appScreens.size > 0;
|
|
1515
|
+
/**
|
|
1516
|
+
* E MENOS TELAS DO QUE A LINHA TAMBÉM NÃO É TESTE - rodadas 6 a 10 do refinamento, 27/09.
|
|
1517
|
+
*
|
|
1518
|
+
* A pergunta da regra é "duas telas compartilham isto?", e num app de UMA tela ela não tem como ser
|
|
1519
|
+
* respondida: todo componente sai "composed on one screen only". Nos cinco estilos das rodadas
|
|
1520
|
+
* (Tailwind, variáveis CSS, CSS Modules, atômico, SCSS) o import tirou assim os componentes do
|
|
1521
|
+
* usuário, e o sistema nascia sem nenhum dele. É o mesmo raciocínio da biblioteca logo acima, do outro
|
|
1522
|
+
* lado: sem telas bastantes para comparar, não há compartilhamento para medir.
|
|
1523
|
+
*/
|
|
1524
|
+
const measurable = appScreens.size >= SCREENS_FOR_SYSTEM;
|
|
1525
|
+
if (hasScreens && measurable) {
|
|
1493
1526
|
const keep = [];
|
|
1494
1527
|
for (const d of defined) {
|
|
1495
1528
|
/**
|
package/dist/commands/refit.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { access, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import { speakInHisProject } from "../in-his-form.js";
|
|
2
3
|
import { basename, join } from "node:path";
|
|
3
4
|
import { generateComponentFiles } from "../component-codegen.js";
|
|
4
5
|
import { readProjectConfig, resolveRegistry } from "../config.js";
|
|
@@ -153,13 +154,15 @@ export async function refit(file, opts) {
|
|
|
153
154
|
console.log(` The recipe is saved in your system's draft either way - only the .tsx was not written.`);
|
|
154
155
|
return;
|
|
155
156
|
}
|
|
156
|
-
|
|
157
|
-
|
|
157
|
+
/** Na grafia e na forma dele - ver `speakInHisProject` (28/09). */
|
|
158
|
+
const spoken = await speakInHisProject(root, compDir, files.map((x) => ({ filename: x.filename, content: x.code })), flavourOf(res.name), res.name, res.name);
|
|
159
|
+
for (const f of spoken) {
|
|
160
|
+
await writeFile(join(compDir, f.filename), f.content, "utf8");
|
|
158
161
|
}
|
|
159
162
|
/** O fingerprint do que escrevemos - protege a edição dele no upgrade (T7). */
|
|
160
|
-
await recordWritten(join(root, "_synthesisui", "ds", slug), res.name,
|
|
163
|
+
await recordWritten(join(root, "_synthesisui", "ds", slug), res.name, spoken);
|
|
161
164
|
materialized = true;
|
|
162
|
-
console.log(`✓ ${config.componentsDir}/${res.name}/ → ${
|
|
165
|
+
console.log(`✓ ${config.componentsDir}/${res.name}/ → ${spoken.map((f) => f.filename).join(", ")} (styles: ${config.styles})`);
|
|
163
166
|
}
|
|
164
167
|
// The materialized component references this system's tokens - without the
|
|
165
168
|
// install (tokens.css + scope) it renders unstyled. Say so, concretely.
|
package/dist/commands/upgrade.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { readdir, readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import { speakInHisProject } from "../in-his-form.js";
|
|
2
3
|
import { join } from "node:path";
|
|
3
4
|
import { pinnedHookVersion, wireAgent } from "../agent-wiring.js";
|
|
4
5
|
import { agentsToMaintain } from "../agents-chosen.js";
|
|
@@ -428,7 +429,7 @@ export async function upgrade(asked, opts) {
|
|
|
428
429
|
const tsxPath = join(componentsRoot, entry, `${entry}.tsx`);
|
|
429
430
|
let head = "";
|
|
430
431
|
try {
|
|
431
|
-
head = (await readFile(tsxPath, "utf8")).slice(0,
|
|
432
|
+
head = (await readFile(tsxPath, "utf8")).slice(0, 2000);
|
|
432
433
|
}
|
|
433
434
|
catch {
|
|
434
435
|
continue; // not a materialized component folder
|
|
@@ -444,12 +445,15 @@ export async function upgrade(asked, opts) {
|
|
|
444
445
|
if (keptEntries.has(entry))
|
|
445
446
|
continue;
|
|
446
447
|
try {
|
|
447
|
-
|
|
448
|
+
/** A receita pelo cabeçalho, não pela pasta - ver `recipeNameOf` (28/09). */
|
|
449
|
+
const res = await fetchComponent(base, slug, recipeNameOf(head, entry));
|
|
448
450
|
const { files } = generateComponentFiles(slug, res.name, res.recipe, res.css, res.version, flavourOf(res.name),
|
|
449
451
|
// Both were missing here, and `upgrade` is the command that REWRITES
|
|
450
452
|
// components somebody already has: without the convention it would have
|
|
451
453
|
// taken a working component and stripped its styles.
|
|
452
|
-
await reactMajorOf(root), res.classNames ?? (await readInstalledConvention(root, slug)),
|
|
454
|
+
await reactMajorOf(root), res.classNames ?? (await readInstalledConvention(root, slug)),
|
|
455
|
+
/** O nome local é a PASTA dele (`--as`), não a receita - senão sai `button.tsx` dentro de `button-gen/`. */
|
|
456
|
+
entry, await readInstalledScheme(root, slug), tongue, themeVars);
|
|
453
457
|
/**
|
|
454
458
|
* A NOTA DAS DECLARAÇÕES NÃO INTERPRETADAS SOBREVIVE AO UPGRADE.
|
|
455
459
|
*
|
|
@@ -461,12 +465,13 @@ export async function upgrade(asked, opts) {
|
|
|
461
465
|
*/
|
|
462
466
|
const nota = unreadComment(unreadForComponent(await readCensus(root), res.name));
|
|
463
467
|
/** Os bytes finais, para o fingerprint lembrar EXATAMENTE o que foi a disco. */
|
|
464
|
-
|
|
468
|
+
/** Na grafia e na forma dele, como o `component` - ver `speakInHisProject` (28/09). */
|
|
469
|
+
const finalFiles = await speakInHisProject(root, join(componentsRoot, entry), files.map((file) => ({
|
|
465
470
|
filename: file.filename,
|
|
466
471
|
content: file.filename.endsWith(".tsx")
|
|
467
472
|
? `${nota}${file.code}`
|
|
468
473
|
: file.code,
|
|
469
|
-
}));
|
|
474
|
+
})), flavourOf(res.name), entry, res.name);
|
|
470
475
|
for (const file of finalFiles) {
|
|
471
476
|
await writeFile(join(componentsRoot, entry, file.filename), file.content, "utf8");
|
|
472
477
|
}
|
|
@@ -567,3 +572,12 @@ export async function upgrade(asked, opts) {
|
|
|
567
572
|
});
|
|
568
573
|
console.log("");
|
|
569
574
|
}
|
|
575
|
+
/**
|
|
576
|
+
* A RECEITA DE UM COMPONENTE GERADO, LIDA NO CABEÇALHO QUE O GERADOR ESCREVEU - revisão do #1629, 28/09.
|
|
577
|
+
*
|
|
578
|
+
* `--as button-gen` põe o componente numa pasta com o nome dele, e o `upgrade` pedia ao registry o componente
|
|
579
|
+
* `button-gen`: "Could not regenerate (...) (removed/renamed in v2?)". Sem cabeçalho, a pasta é a melhor pista.
|
|
580
|
+
*/
|
|
581
|
+
export function recipeNameOf(head, entry) {
|
|
582
|
+
return /Generated by SynthesisUI - "([^"]+)" from the/.exec(head)?.[1] ?? entry;
|
|
583
|
+
}
|
|
@@ -1690,7 +1690,7 @@ themeVars) {
|
|
|
1690
1690
|
filename: `${localName}.tsx`,
|
|
1691
1691
|
code: `${emitCssMode(slug, name, recipe, version, props, convention, localName)}\n`,
|
|
1692
1692
|
});
|
|
1693
|
-
files.push({ filename: `${localName}.css`, code: `${css}\n` });
|
|
1693
|
+
files.push({ filename: `${localName}.css`, code: `${withoutOurScope(css)}\n` });
|
|
1694
1694
|
}
|
|
1695
1695
|
else {
|
|
1696
1696
|
files.push({
|
|
@@ -1717,6 +1717,18 @@ themeVars) {
|
|
|
1717
1717
|
};
|
|
1718
1718
|
return speak(files, tongue, styles === "tailwind" ? "tailwind-class" : "stylesheet", root);
|
|
1719
1719
|
}
|
|
1720
|
+
/**
|
|
1721
|
+
* A FOLHA DO COMPONENTE SEM O NOSSO ESCOPO - medido em 28/09, provando a saída SCSS da rodada 10.
|
|
1722
|
+
*
|
|
1723
|
+
* A folha compilada chega como `[data-ds="<slug>"] .ds-button { … }`, e nada no app dele tem `data-ds`: o
|
|
1724
|
+
* componente gerado não o põe (pôr o nosso escopo na árvore dele é o que `INV-GERAL-13` proíbe). O build
|
|
1725
|
+
* de um Next com o botão gerado deu `<button class="ds-button">` e a regra escopada sem nada que a casasse
|
|
1726
|
+
* - o botão sem estilo, em todo projeto sem Tailwind. A página do `template` nasce dentro de um invólucro
|
|
1727
|
+
* com o atributo e continua escopada; o componente não tem invólucro, e a classe `ds-*` já é só dele.
|
|
1728
|
+
*/
|
|
1729
|
+
function withoutOurScope(css) {
|
|
1730
|
+
return css.replace(/\[data-ds=(["']?)[^\]"']+\1\]\s*/g, "");
|
|
1731
|
+
}
|
|
1720
1732
|
/**
|
|
1721
1733
|
* A TRADUÇÃO, SOBRE OS BYTES, UMA VEZ - e o destino é de quem GEROU o texto, não da extensão.
|
|
1722
1734
|
*
|
|
@@ -18,6 +18,18 @@
|
|
|
18
18
|
* so this returns all of them and the person picks which one has PRIORITY. That word is
|
|
19
19
|
* theirs and it is the right one: the others are still true, they just do not decide.
|
|
20
20
|
*/
|
|
21
|
+
/**
|
|
22
|
+
* A PASTA DA FORMA `flat` - ela casa no lugar que TEM uma `components/`, e as peças moram DENTRO dela. Dizer
|
|
23
|
+
* "directly under `.`" num Next mandava o agente para a raiz (rodadas 6 a 10, 27/09). Quando o caminho já
|
|
24
|
+
* termina na pasta, ele é a resposta.
|
|
25
|
+
*/
|
|
26
|
+
function flatFolder(where, folder) {
|
|
27
|
+
if (!folder)
|
|
28
|
+
return where;
|
|
29
|
+
if (where === "." || where === "")
|
|
30
|
+
return folder;
|
|
31
|
+
return where === folder || where.endsWith(`/${folder}`) ? where : `${where}/${folder}`;
|
|
32
|
+
}
|
|
21
33
|
const SHAPES = [
|
|
22
34
|
{
|
|
23
35
|
kind: "atomic",
|
|
@@ -49,7 +61,7 @@ const SHAPES = [
|
|
|
49
61
|
{
|
|
50
62
|
kind: "flat",
|
|
51
63
|
needs: [["components"]],
|
|
52
|
-
describe: (
|
|
64
|
+
describe: (a, where) => `Every component sits directly under \`${flatFolder(where, a.evidence[0])}\`, one folder deep. It is the shape that stays legible longest while a system is small, and the one to revisit when the folder passes about forty names.`,
|
|
53
65
|
},
|
|
54
66
|
];
|
|
55
67
|
/**
|
|
@@ -203,7 +215,14 @@ function explains(other, shape) {
|
|
|
203
215
|
return other.kind === shape.kind || shape.kind === "flat";
|
|
204
216
|
return (pathKey(other.root) === pathKey(shape.root) &&
|
|
205
217
|
shape.kind === "flat" &&
|
|
206
|
-
other.kind !== "flat"
|
|
218
|
+
other.kind !== "flat" &&
|
|
219
|
+
/**
|
|
220
|
+
* `colocated` NÃO EXPLICA A PASTA COMPARTILHADA - rodadas 6 a 10 do refinamento, 27/09. Num Next, a
|
|
221
|
+
* raiz tem `app/` e `components/`, as duas formas casam ali, e a `flat` sumia: o import dizia "BESIDE
|
|
222
|
+
* THE ROUTE" nos cinco estilos, com os componentes todos em `components/`. `colocated` diz onde moram
|
|
223
|
+
* as rotas; `flat` diz onde moram as peças que elas compartilham.
|
|
224
|
+
*/
|
|
225
|
+
other.kind !== "colocated");
|
|
207
226
|
}
|
|
208
227
|
/**
|
|
209
228
|
* O CAMINHO COMPARÁVEL - a raiz do escopo é escrita `.` pela caminhada, e comparada com `""` ela é
|
|
@@ -395,7 +414,15 @@ entries, scope) {
|
|
|
395
414
|
const hasSrc = entries.some((e) => e.path === "src" ||
|
|
396
415
|
e.path.startsWith("src/") ||
|
|
397
416
|
(isRoot(e.path) && e.dirs.includes("src")));
|
|
398
|
-
|
|
417
|
+
/**
|
|
418
|
+
* O ESCOPO QUE JÁ É A PASTA DE COMPONENTES não ganha outra dentro dele - rodadas 6 e 8, 27/09: a
|
|
419
|
+
* sugestão saía `components/ui/components/{atoms, molecules, organisms}` e `components/components/…`, e o
|
|
420
|
+
* agente da rodada 8 chamou de "a slip in the tool".
|
|
421
|
+
*/
|
|
422
|
+
const insideComponents = /(^|\/)components(\/|$)/.test(base);
|
|
423
|
+
const trunk = insideComponents
|
|
424
|
+
? base
|
|
425
|
+
: `${base ? `${base}/` : ""}${hasSrc ? "src/components" : "components"}`;
|
|
399
426
|
const fresh = `${trunk}/{atoms, molecules, organisms}`;
|
|
400
427
|
/** Se a proposta é o que já existe, ela não é uma pasta nova - e oferecê-la seria repetir a opção 1. */
|
|
401
428
|
return found.some((a) => componentHome(a, scope) === fresh) ? null : fresh;
|
|
@@ -447,6 +447,18 @@ export function partialCandidates(specifier) {
|
|
|
447
447
|
`${dir}${base}.css`,
|
|
448
448
|
];
|
|
449
449
|
}
|
|
450
|
+
/**
|
|
451
|
+
* OS VALORES QUE TIPAM UMA PROP - `kind?: "primary" | "secondary"` -> `["primary", "secondary"]`.
|
|
452
|
+
*
|
|
453
|
+
* Só a união de literais escrita no próprio arquivo: é o que torna a classe escolhida em tempo de execução
|
|
454
|
+
* legível sem adivinhar (rodadas 7 e 8 do refinamento, 27/09).
|
|
455
|
+
*/
|
|
456
|
+
export function unionOfProp(source, prop) {
|
|
457
|
+
const m = new RegExp(`\\b${prop}\\??\\s*:\\s*((?:["'][^"']+["']\\s*\\|?\\s*)+)`).exec(source);
|
|
458
|
+
if (!m)
|
|
459
|
+
return [];
|
|
460
|
+
return [...m[1].matchAll(/["']([^"']+)["']/g)].map((v) => v[1]);
|
|
461
|
+
}
|
|
450
462
|
/**
|
|
451
463
|
* A generic in type position, not an element.
|
|
452
464
|
*
|
|
@@ -614,11 +626,26 @@ export function readModuleUsage(source, local) {
|
|
|
614
626
|
continue;
|
|
615
627
|
}
|
|
616
628
|
const body = event.body;
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
629
|
+
/**
|
|
630
|
+
* `styles[key]` ESCOLHE A CLASSE EM TEMPO DE EXECUÇÃO - e o nome dela está no arquivo, pela união que
|
|
631
|
+
* tipa a prop. Era pulado ("Impossible rather than hard"), e o Button e o Pill do teste-import-modules
|
|
632
|
+
* chegavam sem as cores dos variantes (rodadas 7 e 8, 27/09). Uma chave que não é uma prop tipada
|
|
633
|
+
* continua sem leitura.
|
|
634
|
+
*/
|
|
635
|
+
const picked = [];
|
|
636
|
+
for (const m of body.matchAll(new RegExp(`\\b${local}\\[\\s*([A-Za-z_$][\\w$]*)\\s*\\]`, "g"))) {
|
|
637
|
+
const prop = m[1];
|
|
638
|
+
const values = unionOfProp(source, prop);
|
|
639
|
+
if (values.length === 0)
|
|
640
|
+
continue;
|
|
641
|
+
picked.push({
|
|
642
|
+
prop,
|
|
643
|
+
whenTrue: null,
|
|
644
|
+
whenFalse: null,
|
|
645
|
+
options: Object.fromEntries(values.map((v) => [v, v])),
|
|
646
|
+
});
|
|
621
647
|
}
|
|
648
|
+
bracket.lastIndex = 0;
|
|
622
649
|
ref.lastIndex = 0;
|
|
623
650
|
const named = [...body.matchAll(ref)].map((m) => m[1]);
|
|
624
651
|
if (named.length > 0) {
|
|
@@ -642,6 +669,7 @@ export function readModuleUsage(source, local) {
|
|
|
642
669
|
// The resting class is the first one that is not a ternary branch.
|
|
643
670
|
const inTernary = new Set(variants.flatMap((v) => [v.whenTrue, v.whenFalse].filter(Boolean)));
|
|
644
671
|
const resting = named.find((n) => !inTernary.has(n)) ?? named[0];
|
|
672
|
+
variants.push(...picked);
|
|
645
673
|
out.push({ className: resting, depth, tag: event.tag, variants });
|
|
646
674
|
}
|
|
647
675
|
depth += 1;
|
|
@@ -729,6 +757,7 @@ export function transcribeModule(read, usage, declared) {
|
|
|
729
757
|
for (const [option, className] of [
|
|
730
758
|
["true", v.whenTrue],
|
|
731
759
|
["false", v.whenFalse],
|
|
760
|
+
...Object.entries(v.options ?? {}),
|
|
732
761
|
]) {
|
|
733
762
|
if (!className)
|
|
734
763
|
continue;
|
|
@@ -736,10 +765,18 @@ export function transcribeModule(read, usage, declared) {
|
|
|
736
765
|
if (!branch)
|
|
737
766
|
continue;
|
|
738
767
|
const style = resolve(branch.base);
|
|
739
|
-
if (Object.keys(style).length
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
768
|
+
if (Object.keys(style).length > 0) {
|
|
769
|
+
count += Object.keys(style).length;
|
|
770
|
+
layers.push({ when: { variant: { [v.prop]: option } }, style });
|
|
771
|
+
}
|
|
772
|
+
/** E os estados daquela classe (`.primary:hover`), sob a mesma opção - 27/09. */
|
|
773
|
+
for (const [state, block] of Object.entries(branch.states ?? {})) {
|
|
774
|
+
const st = resolve(block);
|
|
775
|
+
if (Object.keys(st).length === 0)
|
|
776
|
+
continue;
|
|
777
|
+
count += Object.keys(st).length;
|
|
778
|
+
layers.push({ when: { variant: { [v.prop]: option }, state }, style: st });
|
|
779
|
+
}
|
|
743
780
|
}
|
|
744
781
|
}
|
|
745
782
|
}
|
package/dist/doctor/scan.js
CHANGED
|
@@ -495,6 +495,24 @@ function scanCore(file, source, table) {
|
|
|
495
495
|
const findings = [];
|
|
496
496
|
const phantoms = [];
|
|
497
497
|
const ownDeclared = [];
|
|
498
|
+
/**
|
|
499
|
+
* OS NOMES SASS DELE, VISTOS PELA LENTE - rodada 10 do refinamento, 27/09. Num `.scss` lido por `scss-lens.ts`,
|
|
500
|
+
* `$accent` chega aqui como `var(--accent)`, e o nome Sass é conhecido pela tabela em `aliases` (o mapa que
|
|
501
|
+
* DIZ o nome dele e nunca o escreve). Um uso desses conta como vocabulário dele para a cobertura - que dizia
|
|
502
|
+
* "0% token coverage" com todo valor escrito por uma `$variável` - sem nunca entrar como troca.
|
|
503
|
+
*/
|
|
504
|
+
const sassDeclared = new Map();
|
|
505
|
+
for (const [key, their] of table.aliases ?? [])
|
|
506
|
+
for (const n of [their.name, ...(their.also ?? [])])
|
|
507
|
+
if (n.startsWith("$"))
|
|
508
|
+
sassDeclared.set(`--${n.slice(1).toLowerCase()}`, key.split(":").slice(1).join(":"));
|
|
509
|
+
for (const [n, v] of table.sassNames ?? [])
|
|
510
|
+
if (!sassDeclared.has(n))
|
|
511
|
+
sassDeclared.set(n, v);
|
|
512
|
+
for (const [n, v] of table.tsNames ?? [])
|
|
513
|
+
if (!sassDeclared.has(n))
|
|
514
|
+
sassDeclared.set(n, v);
|
|
515
|
+
const sassCounted = new Set();
|
|
498
516
|
const ownCandidates = new Map();
|
|
499
517
|
let tokenUses = 0;
|
|
500
518
|
// Reason by reason. Rolling two into "A or B" was the one place the report
|
|
@@ -570,6 +588,20 @@ function scanCore(file, source, table) {
|
|
|
570
588
|
}
|
|
571
589
|
for (const m of line.matchAll(ANY_VAR_USE)) {
|
|
572
590
|
const name = m[1].toLowerCase();
|
|
591
|
+
/**
|
|
592
|
+
* O NOME SASS CONTA ANTES DO PULO - com um sistema instalado, `--accent` também pode estar na tabela
|
|
593
|
+
* do sistema, e aí o uso era pulado aqui e não entrava em conta nenhuma (0 do sistema, 12 dele, com
|
|
594
|
+
* 80 usos no arquivo). Um `$accent` escrito num `.scss` é vocabulário DELE.
|
|
595
|
+
*/
|
|
596
|
+
const sass = sassDeclared.get(name);
|
|
597
|
+
if (sass !== undefined) {
|
|
598
|
+
ownCandidates.set(name, (ownCandidates.get(name) ?? 0) + 1);
|
|
599
|
+
if (!sassCounted.has(name)) {
|
|
600
|
+
sassCounted.add(name);
|
|
601
|
+
ownDeclared.push({ name, value: sass });
|
|
602
|
+
}
|
|
603
|
+
continue;
|
|
604
|
+
}
|
|
573
605
|
if (isKnownToken(name, table))
|
|
574
606
|
continue;
|
|
575
607
|
ownCandidates.set(name, (ownCandidates.get(name) ?? 0) + 1);
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A LENTE DO SCSS - as `$variáveis` dele lidas como o vocabulário que são (rodada 10 do refinamento, 27/09).
|
|
3
|
+
*
|
|
4
|
+
* O teste-import-scss declarava 20 `$variáveis` em `styles/_tokens.scss` e o censo saiu com `declared: {}`: o
|
|
5
|
+
* vocabulário só era lido de `--x` em `:root`/`@theme`, e o doctor dizia "0% token coverage" com todo valor
|
|
6
|
+
* novo escrito por uma `$variável`. Uma `$variável` some na compilação, mas é um nome que ELE deu a um valor -
|
|
7
|
+
* a mesma decisão que um `--x` registra.
|
|
8
|
+
*
|
|
9
|
+
* A LENTE SÓ EXISTE NA LEITURA. `$nome: valor;` no topo da folha vira `:root { --nome: valor; }`, e cada uso
|
|
10
|
+
* de `$nome` vira `var(--nome)`. Todo o resto do leitor - o vocabulário declarado, a cobertura, a ligação de
|
|
11
|
+
* um valor com o token dele - já sabe ler variável CSS, e passa a ler SCSS sem uma segunda implementação. O
|
|
12
|
+
* arquivo dele não é tocado; quem escreve de volta é o gerador, na grafia dele (`scss-speech.ts`).
|
|
13
|
+
*
|
|
14
|
+
* Só as declarações do NÍVEL DE FORA contam como vocabulário: uma `$x` dentro de um `@mixin` ou de uma regra
|
|
15
|
+
* é local, e chamar isso de token seria inventar um nome que ele não publicou.
|
|
16
|
+
*/
|
|
17
|
+
const DECL = /^\s*\$([A-Za-z_][\w-]*)\s*:\s*([^;]+?)\s*(?:!default|!global)?\s*;/;
|
|
18
|
+
const USE = /\$([A-Za-z_][\w-]*)/g;
|
|
19
|
+
/** As declarações do nível de fora, na ordem, sem seguir aliases. */
|
|
20
|
+
function topLevel(scss) {
|
|
21
|
+
const out = [];
|
|
22
|
+
let depth = 0;
|
|
23
|
+
scss.split("\n").forEach((text, line) => {
|
|
24
|
+
if (depth === 0) {
|
|
25
|
+
const m = DECL.exec(text);
|
|
26
|
+
if (m)
|
|
27
|
+
out.push({ name: m[1], value: m[2].trim(), line });
|
|
28
|
+
}
|
|
29
|
+
for (const ch of text) {
|
|
30
|
+
if (ch === "{")
|
|
31
|
+
depth += 1;
|
|
32
|
+
else if (ch === "}")
|
|
33
|
+
depth = Math.max(0, depth - 1);
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
return out;
|
|
37
|
+
}
|
|
38
|
+
/** `$nome` -> o valor, com os aliases dele seguidos (`$accent: $teal-500` -> `#0e9f91`). */
|
|
39
|
+
export function scssVariables(scss) {
|
|
40
|
+
const raw = new Map(topLevel(scss).map((d) => [`$${d.name}`, d.value]));
|
|
41
|
+
const out = new Map();
|
|
42
|
+
for (const name of raw.keys()) {
|
|
43
|
+
const seen = new Set([name]);
|
|
44
|
+
let value = raw.get(name);
|
|
45
|
+
for (let ref = /^\$([A-Za-z_][\w-]*)$/.exec(value); ref; ref = /^\$([A-Za-z_][\w-]*)$/.exec(value)) {
|
|
46
|
+
const next = raw.get(`$${ref[1]}`);
|
|
47
|
+
if (next === undefined || seen.has(`$${ref[1]}`))
|
|
48
|
+
break;
|
|
49
|
+
seen.add(`$${ref[1]}`);
|
|
50
|
+
value = next;
|
|
51
|
+
}
|
|
52
|
+
out.set(name, value);
|
|
53
|
+
}
|
|
54
|
+
return out;
|
|
55
|
+
}
|
|
56
|
+
/** A folha como o leitor de CSS a entende - ver o cabeçalho. Sem `$`, ela volta byte a byte. */
|
|
57
|
+
export function scssAsCustomProperties(scss) {
|
|
58
|
+
if (!scss.includes("$"))
|
|
59
|
+
return scss;
|
|
60
|
+
const decls = new Map(topLevel(scss).map((d) => [d.line, d]));
|
|
61
|
+
return scss
|
|
62
|
+
.split("\n")
|
|
63
|
+
.map((text, line) => {
|
|
64
|
+
const d = decls.get(line);
|
|
65
|
+
if (d)
|
|
66
|
+
return `:root { --${d.name}: ${d.value.replace(USE, "var(--$1)")}; }`;
|
|
67
|
+
if (/^\s*@(use|forward|import)\b/.test(text))
|
|
68
|
+
return text;
|
|
69
|
+
return text.replace(USE, "var(--$1)");
|
|
70
|
+
})
|
|
71
|
+
.join("\n");
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* UMA FOLHA DELE, LIDA COMO O LEITOR A ENTENDE - a porta única da lente. Todo leitor de folha do usuário
|
|
75
|
+
* passa por aqui: o censo, o vocabulário do gerador, o `their-vars`, o doctor e o fim do turno. Um leitor
|
|
76
|
+
* que lesse o `.scss` cru veria "0% token coverage" onde os outros veem o vocabulário dele.
|
|
77
|
+
*/
|
|
78
|
+
export async function readStyleSheet(file) {
|
|
79
|
+
const { readFile } = await import("node:fs/promises");
|
|
80
|
+
const text = await readFile(file, "utf8").catch(() => "");
|
|
81
|
+
return /\.(scss|sass)$/i.test(file) ? scssAsCustomProperties(text) : text;
|
|
82
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { unionOfProp } from "./css-modules.js";
|
|
2
|
+
const PIECE = /([A-Za-z][\w-]*?)\$\{\s*([A-Za-z_$][\w$]*)\s*\}/g;
|
|
3
|
+
export function templateClassVariants(src, classes) {
|
|
4
|
+
const layers = [];
|
|
5
|
+
const claimed = new Set();
|
|
6
|
+
const seen = new Set();
|
|
7
|
+
for (const tpl of src.matchAll(/className=\{`([^`]*)`\}/g)) {
|
|
8
|
+
for (const m of tpl[1].matchAll(PIECE)) {
|
|
9
|
+
const prefix = m[1];
|
|
10
|
+
const prop = m[2];
|
|
11
|
+
if (seen.has(`${prefix}|${prop}`))
|
|
12
|
+
continue;
|
|
13
|
+
seen.add(`${prefix}|${prop}`);
|
|
14
|
+
for (const value of unionOfProp(src, prop)) {
|
|
15
|
+
const rule = classes.get(`${prefix}${value}`);
|
|
16
|
+
if (!rule)
|
|
17
|
+
continue;
|
|
18
|
+
claimed.add(`${prefix}${value}`);
|
|
19
|
+
if (Object.keys(rule.base).length > 0)
|
|
20
|
+
layers.push({ when: { variant: { [prop]: value } }, style: { ...rule.base } });
|
|
21
|
+
for (const [state, block] of Object.entries(rule.states))
|
|
22
|
+
if (Object.keys(block).length > 0)
|
|
23
|
+
layers.push({ when: { variant: { [prop]: value }, state }, style: { ...block } });
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return { layers, claimed: [...claimed] };
|
|
28
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
const kebab = (k) => k.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase();
|
|
2
|
+
/** Um literal de objeto a partir de `{`, ou `null` quando aparece algo que não é literal. */
|
|
3
|
+
function parseObject(s, start) {
|
|
4
|
+
let i = start + 1;
|
|
5
|
+
const tree = {};
|
|
6
|
+
const ws = () => {
|
|
7
|
+
for (;;) {
|
|
8
|
+
while (i < s.length && /\s/.test(s[i]))
|
|
9
|
+
i += 1;
|
|
10
|
+
if (s.startsWith("//", i))
|
|
11
|
+
i = s.indexOf("\n", i) === -1 ? s.length : s.indexOf("\n", i);
|
|
12
|
+
else if (s.startsWith("/*", i))
|
|
13
|
+
i = s.indexOf("*/", i) === -1 ? s.length : s.indexOf("*/", i) + 2;
|
|
14
|
+
else
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
const str = () => {
|
|
19
|
+
const q = s[i];
|
|
20
|
+
if (q !== '"' && q !== "'")
|
|
21
|
+
return null;
|
|
22
|
+
const end = s.indexOf(q, i + 1);
|
|
23
|
+
if (end === -1)
|
|
24
|
+
return null;
|
|
25
|
+
const out = s.slice(i + 1, end);
|
|
26
|
+
i = end + 1;
|
|
27
|
+
return out;
|
|
28
|
+
};
|
|
29
|
+
for (;;) {
|
|
30
|
+
ws();
|
|
31
|
+
if (s[i] === "}")
|
|
32
|
+
return { tree, end: i + 1 };
|
|
33
|
+
let key = str();
|
|
34
|
+
if (key === null) {
|
|
35
|
+
const m = /^[A-Za-z_$][\w$]*|^\d+/.exec(s.slice(i));
|
|
36
|
+
if (!m)
|
|
37
|
+
return null;
|
|
38
|
+
key = m[0];
|
|
39
|
+
i += key.length;
|
|
40
|
+
}
|
|
41
|
+
ws();
|
|
42
|
+
if (s[i] !== ":")
|
|
43
|
+
return null;
|
|
44
|
+
i += 1;
|
|
45
|
+
ws();
|
|
46
|
+
if (s[i] === "{") {
|
|
47
|
+
const inner = parseObject(s, i);
|
|
48
|
+
if (!inner)
|
|
49
|
+
return null;
|
|
50
|
+
tree[key] = inner.tree;
|
|
51
|
+
i = inner.end;
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
const text = str();
|
|
55
|
+
if (text !== null)
|
|
56
|
+
tree[key] = text;
|
|
57
|
+
else {
|
|
58
|
+
const m = /^-?\d+(\.\d+)?/.exec(s.slice(i));
|
|
59
|
+
if (!m)
|
|
60
|
+
return null;
|
|
61
|
+
tree[key] = Number(m[0]);
|
|
62
|
+
i += m[0].length;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
ws();
|
|
66
|
+
if (s[i] === ",")
|
|
67
|
+
i += 1;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
const DESIGN = /^(#[0-9a-f]{3,8}|rgba?\(|hsla?\(|oklch\(|cubic-bezier\(|-?\d+(\.\d+)?(px|rem|em|%|ms|s)\b)|,|['"]/i;
|
|
71
|
+
function flatten(tree, prefix, out) {
|
|
72
|
+
for (const [k, v] of Object.entries(tree)) {
|
|
73
|
+
const path = [...prefix, kebab(k)];
|
|
74
|
+
if (typeof v === "object")
|
|
75
|
+
flatten(v, path, out);
|
|
76
|
+
else
|
|
77
|
+
out.set(`--${path.join("-")}`, typeof v === "number" ? `${v}px` : v);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
/** Os objetos exportados que seguram valores de design - um objeto de rotas ou de textos não conta. */
|
|
81
|
+
export function tsTokenObjects(source) {
|
|
82
|
+
const out = [];
|
|
83
|
+
for (const m of source.matchAll(/export\s+const\s+([A-Za-z_$][\w$]*)\s*(?::[^=]+)?=\s*\{/g)) {
|
|
84
|
+
const parsed = parseObject(source, (m.index ?? 0) + m[0].length - 1);
|
|
85
|
+
if (!parsed)
|
|
86
|
+
continue;
|
|
87
|
+
const values = new Map();
|
|
88
|
+
flatten(parsed.tree, [], values);
|
|
89
|
+
const leaves = [...values.values()];
|
|
90
|
+
const design = leaves.filter((v) => DESIGN.test(v) || /^\d+(\.\d+)?px$/.test(v)).length;
|
|
91
|
+
if (leaves.length >= 4 && design * 10 >= leaves.length * 6)
|
|
92
|
+
out.push({ name: m[1], values });
|
|
93
|
+
}
|
|
94
|
+
return out;
|
|
95
|
+
}
|
|
96
|
+
/** Os nomes locais pelos quais este arquivo importa cada objeto de tokens - `{ tokens as t }` -> `t`. */
|
|
97
|
+
function localNames(source, objects) {
|
|
98
|
+
const out = new Map();
|
|
99
|
+
for (const m of source.matchAll(/import\s*\{([^}]*)\}\s*from\s*["'][^"']+["']/g))
|
|
100
|
+
for (const part of m[1].split(",")) {
|
|
101
|
+
const [imported, local] = part.trim().split(/\s+as\s+/).map((x) => x.trim());
|
|
102
|
+
const found = objects.find((o) => o.name === imported);
|
|
103
|
+
if (found && imported)
|
|
104
|
+
out.set(local || imported, found);
|
|
105
|
+
}
|
|
106
|
+
return out;
|
|
107
|
+
}
|
|
108
|
+
/** O arquivo como o leitor de estilo o entende - ver o cabeçalho. Sem objeto importado, ele volta igual. */
|
|
109
|
+
export function tsTokensAsVars(source, objects) {
|
|
110
|
+
const locals = localNames(source, objects);
|
|
111
|
+
if (locals.size === 0)
|
|
112
|
+
return source;
|
|
113
|
+
const names = [...locals.keys()].map((n) => n.replace(/\$/g, "\\$")).join("|");
|
|
114
|
+
const member = new RegExp(`\\b(${names})\\.([A-Za-z_$][\\w$.]*)`);
|
|
115
|
+
const varOf = (local, path) => {
|
|
116
|
+
const key = `--${path.split(".").map(kebab).join("-")}`;
|
|
117
|
+
return locals.get(local)?.values.has(key) ? `var(${key})` : null;
|
|
118
|
+
};
|
|
119
|
+
let out = source.replace(/`([^`]*)`/g, (whole, body) => {
|
|
120
|
+
if (!body.includes("${"))
|
|
121
|
+
return whole;
|
|
122
|
+
let ok = true;
|
|
123
|
+
const text = body.replace(/\$\{([^}]*)\}([a-z%]*)/g, (_p, expr) => {
|
|
124
|
+
const m = new RegExp(`^\\s*${member.source}\\s*$`).exec(expr);
|
|
125
|
+
const v = m ? varOf(m[1], m[2]) : null;
|
|
126
|
+
if (!v)
|
|
127
|
+
ok = false;
|
|
128
|
+
return v ?? "";
|
|
129
|
+
});
|
|
130
|
+
return ok ? `"${text}"` : whole;
|
|
131
|
+
});
|
|
132
|
+
out = out.replace(new RegExp(`(:\\s*)${member.source}(?=\\s*[,}\\n])`, "g"), (whole, lead, local, path) => {
|
|
133
|
+
const v = varOf(local, path);
|
|
134
|
+
return v ? `${lead}"${v}"` : whole;
|
|
135
|
+
});
|
|
136
|
+
return out;
|
|
137
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
const kebab = (v) => v.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase();
|
|
2
|
+
const camel = (v) => v.replace(/-([a-z0-9])/g, (_m, c) => c.toUpperCase());
|
|
3
|
+
const escape = (v) => v.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
4
|
+
export function inHisForm(files, form,
|
|
5
|
+
/** O nome que o componente ganha no projeto dele (`--as`), de onde sai a classe numa folha global. */
|
|
6
|
+
componentName,
|
|
7
|
+
/** O nome da receita, de onde sai o nosso endereço interno (`ds-<receita>`). Ausente, é o mesmo. */
|
|
8
|
+
recipeName) {
|
|
9
|
+
if (form.kind === "utility")
|
|
10
|
+
return files;
|
|
11
|
+
const root = `ds-${kebab(recipeName ?? componentName)}`;
|
|
12
|
+
const ours = new Set();
|
|
13
|
+
for (const f of files)
|
|
14
|
+
for (const m of f.content.matchAll(new RegExp(`\\b${escape(root)}(?:-[a-z0-9-]*[a-z0-9])?(?![\\w-])`, "g")))
|
|
15
|
+
ours.add(m[0]);
|
|
16
|
+
/** A mais longa primeiro, para `ds-button-icon-left` não virar `<root>-icon-left`. */
|
|
17
|
+
const classes = [...ours].sort((a, b) => b.length - a.length);
|
|
18
|
+
/**
|
|
19
|
+
* NUMA FOLHA GLOBAL, UM NOME QUE ELE JÁ USA SE MISTURA COM O DELE - provado em 28/09: o teste-import-css-vars
|
|
20
|
+
* já declara `.card` e `.card__title`. Um nome tomado ganha um número, o primeiro livre.
|
|
21
|
+
*/
|
|
22
|
+
/** O prefixo dele, quando a folha global tem um (`acme-card`) - medido pelo censo (`classStyle.prefix`). */
|
|
23
|
+
const own = form.kind === "global" ? `${form.prefix ?? ""}${kebab(componentName)}` : kebab(componentName);
|
|
24
|
+
let base = own;
|
|
25
|
+
if (form.kind === "global" && form.taken) {
|
|
26
|
+
const free = (b) => !form.taken?.has(b);
|
|
27
|
+
for (let n = 2; !free(base); n += 1)
|
|
28
|
+
base = `${own}-${n}`;
|
|
29
|
+
}
|
|
30
|
+
const target = (cls) => {
|
|
31
|
+
const part = cls === root ? null : cls.slice(root.length + 1);
|
|
32
|
+
if (form.kind === "modules")
|
|
33
|
+
return part ? camel(part) : "root";
|
|
34
|
+
return part ? `${base}${form.partSeparator}${part}` : base;
|
|
35
|
+
};
|
|
36
|
+
const sheetOf = (f) => f.filename.endsWith(".css") || f.filename.endsWith(".scss");
|
|
37
|
+
const sheet = files.find(sheetOf);
|
|
38
|
+
const newSheet = sheet && form.kind === "modules"
|
|
39
|
+
? sheet.filename.replace(/\.(s?css)$/, `.module.${form.ext}`)
|
|
40
|
+
: sheet?.filename;
|
|
41
|
+
return files.map((f) => {
|
|
42
|
+
let content = f.content;
|
|
43
|
+
if (sheetOf(f)) {
|
|
44
|
+
for (const cls of classes)
|
|
45
|
+
content = content.replace(new RegExp(`\\.${escape(cls)}(?![\\w-])`, "g"), `.${target(cls)}`);
|
|
46
|
+
return { ...f, filename: newSheet ?? f.filename, content };
|
|
47
|
+
}
|
|
48
|
+
if (!/\.(tsx|jsx)$/.test(f.filename))
|
|
49
|
+
return f;
|
|
50
|
+
if (form.kind === "modules") {
|
|
51
|
+
for (const cls of classes) {
|
|
52
|
+
const key = target(cls);
|
|
53
|
+
content = content
|
|
54
|
+
.replace(new RegExp(`className="${escape(cls)}"`, "g"), `className={styles.${key}}`)
|
|
55
|
+
.replace(new RegExp(`"${escape(cls)}"`, "g"), `styles.${key}`);
|
|
56
|
+
}
|
|
57
|
+
if (sheet && newSheet)
|
|
58
|
+
content = content.replace(new RegExp(`import\\s+["']\\./${escape(sheet.filename)}["'];?`), `import styles from "./${newSheet}";`);
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
for (const cls of classes)
|
|
62
|
+
content = content.replace(new RegExp(`"${escape(cls)}"`, "g"), `"${target(cls)}"`);
|
|
63
|
+
}
|
|
64
|
+
return { ...f, content };
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* A FORMA DO PROJETO DELE, MEDIDA - e só num projeto DELE. O censo (`_synthesisui/census.json`) só existe
|
|
69
|
+
* depois de um import, que é quando o projeto é dele; num projeto novo nascido de um sistema nosso não há
|
|
70
|
+
* censo, e o gerador escreve na forma do sistema, como sempre (a regra de 27/09, `init` do fork).
|
|
71
|
+
*
|
|
72
|
+
* `utility` com zero utilitário é a leitura que o censo faz de um projeto BEM sem prefixo comum, ou de um de
|
|
73
|
+
* `style` inline (rodadas 7 e 9) - ali não há Tailwind, e o que ele tem é folha global.
|
|
74
|
+
*/
|
|
75
|
+
export async function hisFormOf(root, flavour) {
|
|
76
|
+
if (flavour === "tailwind")
|
|
77
|
+
return { kind: "utility" };
|
|
78
|
+
const { readFile, readdir } = await import("node:fs/promises");
|
|
79
|
+
const { join } = await import("node:path");
|
|
80
|
+
const raw = await readFile(join(root, "_synthesisui", "census.json"), "utf8").catch(() => "");
|
|
81
|
+
if (!raw)
|
|
82
|
+
return { kind: "utility" };
|
|
83
|
+
const census = JSON.parse(raw);
|
|
84
|
+
const style = census.classStyle;
|
|
85
|
+
const sheets = [];
|
|
86
|
+
const walk = async (dir) => {
|
|
87
|
+
for (const e of await readdir(dir, { withFileTypes: true }).catch(() => [])) {
|
|
88
|
+
if (e.name.startsWith(".") || ["node_modules", ".next", "_synthesisui", "dist", "build"].includes(e.name))
|
|
89
|
+
continue;
|
|
90
|
+
const p = join(dir, e.name);
|
|
91
|
+
if (e.isDirectory())
|
|
92
|
+
await walk(p);
|
|
93
|
+
else if (/\.(css|scss|sass)$/i.test(e.name))
|
|
94
|
+
sheets.push(p);
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
await walk(root);
|
|
98
|
+
if (style?.kind === "modules")
|
|
99
|
+
return { kind: "modules", ext: sheets.some((s) => /\.module\.(scss|sass)$/i.test(s)) ? "scss" : "css" };
|
|
100
|
+
/** As classes que as folhas DELE já declaram - nenhuma delas pode ser reusada por um componente gerado. */
|
|
101
|
+
const taken = new Set();
|
|
102
|
+
let bem = false;
|
|
103
|
+
for (const s of sheets) {
|
|
104
|
+
if (/\.module\./i.test(s))
|
|
105
|
+
continue;
|
|
106
|
+
const css = await readFile(s, "utf8").catch(() => "");
|
|
107
|
+
for (const m of css.matchAll(/\.(-?[_a-zA-Z][\w-]*)/g))
|
|
108
|
+
taken.add(m[1]);
|
|
109
|
+
if (/\.[a-z][\w-]*__[a-z]/i.test(css))
|
|
110
|
+
bem = true;
|
|
111
|
+
}
|
|
112
|
+
if (style?.kind === "global")
|
|
113
|
+
return { kind: "global", partSeparator: style.partSeparator || "-", prefix: style.prefix || "", taken };
|
|
114
|
+
if (style?.kind === "utility" && (style.utilities ?? 0) === 0)
|
|
115
|
+
return { kind: "global", partSeparator: bem ? "__" : "-", taken };
|
|
116
|
+
return { kind: "utility" };
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* A PORTA ÚNICA DA ESCRITA NO PROJETO DELE - a grafia (`scss-speech.ts`) e depois a forma (`inHisForm`).
|
|
120
|
+
*
|
|
121
|
+
* Todo comando que grava um componente gerado passa por aqui: `component`, `upgrade`, `refit` e `generate`.
|
|
122
|
+
* Revisão do #1629, 28/09: o `upgrade` regenerava direto do gerador, e num projeto de CSS Modules reescreveria
|
|
123
|
+
* o `.tsx` para importar um `button.css` global com `ds-button` - o componente de volta à nossa forma, e sem a
|
|
124
|
+
* folha dele. Um caminho que escreve sem passar por aqui é um caminho que escreve na nossa forma.
|
|
125
|
+
*/
|
|
126
|
+
export async function speakInHisProject(root, compDir, files, flavour, localName, recipeName) {
|
|
127
|
+
if (flavour === "tailwind")
|
|
128
|
+
return files;
|
|
129
|
+
const { relative } = await import("node:path");
|
|
130
|
+
const { inScssSpeech, scssVocabularyOf } = await import("./scss-speech.js");
|
|
131
|
+
const spoken = inScssSpeech(files, await scssVocabularyOf(root), relative(root, compDir));
|
|
132
|
+
return inHisForm(spoken, await hisFormOf(root, flavour), localName, recipeName);
|
|
133
|
+
}
|
package/dist/install-marks.js
CHANGED
|
@@ -281,7 +281,15 @@
|
|
|
281
281
|
* que o sistema não tem, e o `.lock` de um sistema nunca medido num código nasce com `reading: "none"` -
|
|
282
282
|
* rodada 5 do refinamento. Uma pasta escrita antes ensina `.ds-table` ao agente.
|
|
283
283
|
*/
|
|
284
|
-
|
|
284
|
+
/**
|
|
285
|
+
* 0.16.467 -> 0.16.469 em 28/09, e o passo 1 dá **SIM**: o componente gerado em modo CSS passa a sair sem o
|
|
286
|
+
* nosso `[data-ds]` - medido num build, a regra escopada não casava com nada no app dele e o botão ficava sem
|
|
287
|
+
* estilo, em todo projeto sem Tailwind -, e num projeto SCSS a folha sai `.scss`, com o `@use` da folha de
|
|
288
|
+
* tokens dele e as `$variáveis` dele (rodada 10 do refinamento). Um componente escrito antes está sem estilo.
|
|
289
|
+
* Na mesma versão, o componente sai na FORMA do projeto dele: `.module.(s)css` com `styles.root` onde ele usa
|
|
290
|
+
* CSS Modules, a classe sem o nosso `ds-` onde ele usa folha global (a linha do dono, 28/09 00:49).
|
|
291
|
+
*/
|
|
292
|
+
export const MATERIALISER_SINCE = "0.16.469";
|
|
285
293
|
/**
|
|
286
294
|
* A ÚLTIMA VERSÃO EM QUE O QUE O HOOK RODA MUDOU.
|
|
287
295
|
*
|
|
@@ -897,7 +905,17 @@ export const CHECKER_SINCE = "0.16.467";
|
|
|
897
905
|
* deveriam estar lá. Quem tem censo gravado precisa de um `sync` para a fila de trabalho dele ser a
|
|
898
906
|
* real; ver o livro-razão de `corpus.spec.ts`.
|
|
899
907
|
*/
|
|
900
|
-
|
|
908
|
+
/**
|
|
909
|
+
* 0.16.440 -> 0.16.468 em 27/09, e o passo 1 dá **SIM**: num app de uma tela só a regra de duas telas deixa
|
|
910
|
+
* de tirar os componentes dele - rodadas 6 a 10 do refinamento, nos cinco estilos o sistema nascia sem
|
|
911
|
+
* nenhum componente do usuário. Um censo lido antes guardou essa lista vazia, e só remedir a enche.
|
|
912
|
+
*/
|
|
913
|
+
/**
|
|
914
|
+
* 0.16.468 -> 0.16.469 em 28/09, e o passo 1 dá **SIM**: o censo lê as `$variáveis` do SCSS e o objeto de tokens
|
|
915
|
+
* em TypeScript como o vocabulário dele, e a classe escolhida em tempo de execução pela união que tipa a prop
|
|
916
|
+
* (rodadas 7 a 10 do refinamento). Um censo lido antes guardou `declared: {}` para esses projetos.
|
|
917
|
+
*/
|
|
918
|
+
export const READER_SINCE = "0.16.469";
|
|
901
919
|
/**
|
|
902
920
|
* O QUE ESTÁ INSTALADO AQUI FICOU PARA TRÁS - e as DUAS condições que fazem isso ser verdade.
|
|
903
921
|
*
|
package/dist/project-facts.js
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
* emits. It renders completely unstyled, and nothing anywhere reports a problem.
|
|
15
15
|
*/
|
|
16
16
|
import { readdir, readFile } from "node:fs/promises";
|
|
17
|
+
import { readStyleSheet } from "./doctor/scss-lens.js";
|
|
17
18
|
import { join, relative, sep } from "node:path";
|
|
18
19
|
import { DEFAULT_CONVENTION, ThemeVocab, } from "./component-codegen.js";
|
|
19
20
|
/**
|
|
@@ -344,8 +345,8 @@ export async function themeVocabOf(root, slug, names) {
|
|
|
344
345
|
const path = join(at, entry.name);
|
|
345
346
|
if (entry.isDirectory())
|
|
346
347
|
await walk(path);
|
|
347
|
-
else if (/\.css$/i.test(entry.name)) {
|
|
348
|
-
const css = await
|
|
348
|
+
else if (/\.(css|scss|sass)$/i.test(entry.name)) {
|
|
349
|
+
const css = await readStyleSheet(path);
|
|
349
350
|
if (!THEME_BLOCK.test(css) && !/:root\b/.test(css))
|
|
350
351
|
continue;
|
|
351
352
|
for (const [n, v] of declarationsIn(css, /--(?!ds-)[a-zA-Z0-9_-]+/))
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { readdir, readFile } from "node:fs/promises";
|
|
2
|
+
import { join, relative } from "node:path";
|
|
3
|
+
import { scssVariables } from "./doctor/scss-lens.js";
|
|
4
|
+
const SKIP = new Set(["node_modules", ".next", "dist", "build", "out", "_synthesisui", "coverage"]);
|
|
5
|
+
/** O vocabulário SCSS do projeto: a folha que declara mais `$variáveis` no nível de fora. */
|
|
6
|
+
export async function scssVocabularyOf(root) {
|
|
7
|
+
let best = null;
|
|
8
|
+
const walk = async (dir) => {
|
|
9
|
+
for (const entry of await readdir(dir, { withFileTypes: true }).catch(() => [])) {
|
|
10
|
+
if (entry.name.startsWith(".") || SKIP.has(entry.name))
|
|
11
|
+
continue;
|
|
12
|
+
const path = join(dir, entry.name);
|
|
13
|
+
if (entry.isDirectory())
|
|
14
|
+
await walk(path);
|
|
15
|
+
else if (/\.(scss|sass)$/i.test(entry.name) && !/\.module\./i.test(entry.name)) {
|
|
16
|
+
const vars = scssVariables(await readFile(path, "utf8").catch(() => ""));
|
|
17
|
+
if (vars.size > (best?.vars.size ?? 0))
|
|
18
|
+
best = { vars, tokensFile: relative(root, path).split("\\").join("/") };
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
await walk(root);
|
|
23
|
+
return best;
|
|
24
|
+
}
|
|
25
|
+
/** `styles/_tokens.scss`, visto de `components/button` -> `../../styles/tokens`. */
|
|
26
|
+
function usePath(tokensFile, compDir) {
|
|
27
|
+
const bare = tokensFile.replace(/(^|\/)_([^/]+)$/, "$1$2").replace(/\.(scss|sass)$/i, "");
|
|
28
|
+
const rel = relative(compDir, bare).split("\\").join("/");
|
|
29
|
+
return rel.startsWith(".") ? rel : `./${rel}`;
|
|
30
|
+
}
|
|
31
|
+
export function inScssSpeech(files, vocab, compDir) {
|
|
32
|
+
if (!vocab || vocab.vars.size === 0)
|
|
33
|
+
return files;
|
|
34
|
+
const byValue = new Map();
|
|
35
|
+
for (const [name, value] of vocab.vars) {
|
|
36
|
+
const k = value.trim().toLowerCase();
|
|
37
|
+
byValue.set(k, [...(byValue.get(k) ?? []), name]);
|
|
38
|
+
}
|
|
39
|
+
const renamed = new Map();
|
|
40
|
+
const out = files.map((f) => {
|
|
41
|
+
if (!f.filename.endsWith(".css"))
|
|
42
|
+
return f;
|
|
43
|
+
const filename = f.filename.replace(/\.css$/, ".scss");
|
|
44
|
+
renamed.set(f.filename, filename);
|
|
45
|
+
const body = f.content
|
|
46
|
+
.replace(/var\(\s*--([A-Za-z_][\w-]*)\s*\)/g, (whole, n) => vocab.vars.has(`$${n}`) ? `$${n}` : whole)
|
|
47
|
+
.replace(/:\s*([^;{}]+?)\s*;/g, (whole, value) => {
|
|
48
|
+
const hits = byValue.get(value.trim().toLowerCase());
|
|
49
|
+
return hits && hits.length === 1 ? `: ${hits[0]};` : whole;
|
|
50
|
+
});
|
|
51
|
+
return { ...f, filename, content: `@use "${usePath(vocab.tokensFile, compDir)}" as *;\n\n${body}` };
|
|
52
|
+
});
|
|
53
|
+
return out.map((f) => {
|
|
54
|
+
if (!/\.(tsx|ts|jsx|js)$/.test(f.filename))
|
|
55
|
+
return f;
|
|
56
|
+
let content = f.content;
|
|
57
|
+
for (const [from, to] of renamed)
|
|
58
|
+
content = content.split(`./${from}`).join(`./${to}`);
|
|
59
|
+
return content === f.content ? f : { ...f, content };
|
|
60
|
+
});
|
|
61
|
+
}
|
package/dist/styles-flavour.js
CHANGED
|
@@ -107,7 +107,16 @@ export async function measuredStyle(root) {
|
|
|
107
107
|
return {};
|
|
108
108
|
try {
|
|
109
109
|
const census = JSON.parse(raw);
|
|
110
|
-
|
|
110
|
+
/**
|
|
111
|
+
* `utility` COM ZERO UTILITÁRIO NÃO É TAILWIND - rodada 7 do refinamento, provado em 28/09. É a leitura que o
|
|
112
|
+
* censo faz de um projeto BEM sem prefixo comum, ou de um de `style` inline: o teste-import-css-vars não tem
|
|
113
|
+
* Tailwind, e o `component` saía em utilitários. Com zero contados, o que ele escreve é folha global. O
|
|
114
|
+
* `utility` que manda Tailwind mesmo sem a dependência na raiz (o monorepo) continua valendo - ali há
|
|
115
|
+
* utilitários contados.
|
|
116
|
+
*/
|
|
117
|
+
const kind = census.classStyle?.kind === "utility" && (census.classStyle.utilities ?? 0) === 0
|
|
118
|
+
? "global"
|
|
119
|
+
: census.classStyle?.kind;
|
|
111
120
|
return {
|
|
112
121
|
byComponent: census.coverage?.byComponent,
|
|
113
122
|
classStyle: kind === "global" || kind === "modules" || kind === "utility"
|
package/dist/their-vars.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { readdir, readFile } from "node:fs/promises";
|
|
2
|
+
import { readStyleSheet } from "./doctor/scss-lens.js";
|
|
2
3
|
import { join } from "node:path";
|
|
3
4
|
import { theirNames } from "./doctor/their-names.js";
|
|
4
5
|
import { buildTable, normalizeValue, } from "./doctor/tokens.js";
|
|
@@ -235,7 +236,7 @@ async function harvestTheirCss(root) {
|
|
|
235
236
|
if (entry.isDirectory())
|
|
236
237
|
await walk(path);
|
|
237
238
|
else if (/\.(css|scss|sass|less)$/i.test(entry.name)) {
|
|
238
|
-
const source = await
|
|
239
|
+
const source = await readStyleSheet(path);
|
|
239
240
|
sheets.set(path, source);
|
|
240
241
|
css += `\n${source}`;
|
|
241
242
|
}
|
package/package.json
CHANGED