@bamboocss/config 1.30.0 → 1.31.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/dist/index.cjs +251 -45
- package/dist/index.d.cts +1948 -4
- package/dist/index.d.mts +1948 -4
- package/dist/index.mjs +249 -46
- package/dist/merge-config.cjs +56 -6
- package/dist/merge-config.mjs +56 -6
- package/package.json +7 -7
package/dist/index.mjs
CHANGED
|
@@ -3,11 +3,11 @@ import { BAMBOO_CONFIG_NAME, BambooError, assign, dashCase, isObject, isString,
|
|
|
3
3
|
import { bundleNRequire } from "bundle-n-require";
|
|
4
4
|
import findUp from "escalade/sync";
|
|
5
5
|
import path, { posix, resolve, sep } from "path";
|
|
6
|
+
import { preset as presetBase } from "@bamboocss/preset-base";
|
|
7
|
+
import { preset as presetBamboo } from "@bamboocss/preset-bamboo";
|
|
6
8
|
import microdiff from "microdiff";
|
|
7
9
|
import fs from "fs";
|
|
8
10
|
import ts from "typescript";
|
|
9
|
-
import { preset as presetBase } from "@bamboocss/preset-base";
|
|
10
|
-
import { preset as presetBamboo } from "@bamboocss/preset-bamboo";
|
|
11
11
|
//#region src/is-bamboo-config.ts
|
|
12
12
|
const configName = "bamboo";
|
|
13
13
|
const bambooConfigFiles = new Set([
|
|
@@ -58,6 +58,25 @@ async function bundleConfig(options) {
|
|
|
58
58
|
};
|
|
59
59
|
}
|
|
60
60
|
//#endregion
|
|
61
|
+
//#region src/bundled-preset.ts
|
|
62
|
+
const bundledPresets = {
|
|
63
|
+
"@bamboocss/preset-base": presetBase,
|
|
64
|
+
"@bamboocss/preset-bamboo": presetBamboo,
|
|
65
|
+
"@bamboocss/dev/presets": presetBamboo
|
|
66
|
+
};
|
|
67
|
+
const bundledPresetsNames = Object.keys(bundledPresets);
|
|
68
|
+
const isBundledPreset = (preset) => bundledPresetsNames.includes(preset);
|
|
69
|
+
const getBundledPreset = (preset) => {
|
|
70
|
+
return typeof preset === "string" && isBundledPreset(preset) ? bundledPresets[preset] : void 0;
|
|
71
|
+
};
|
|
72
|
+
/**
|
|
73
|
+
* What `presets` loads when a config does not list any.
|
|
74
|
+
*
|
|
75
|
+
* Exported so a config that adds a preset can keep them without restating them:
|
|
76
|
+
* `presets: [...defaultPresets, myPreset]`. Spread it — the array is shared.
|
|
77
|
+
*/
|
|
78
|
+
const defaultPresets = [presetBase, presetBamboo];
|
|
79
|
+
//#endregion
|
|
61
80
|
//#region src/create-matcher.ts
|
|
62
81
|
/**
|
|
63
82
|
* Acts like a .gitignore matcher
|
|
@@ -477,18 +496,35 @@ const tryCatch = (name, fn) => {
|
|
|
477
496
|
};
|
|
478
497
|
//#endregion
|
|
479
498
|
//#region src/validation/utils.ts
|
|
480
|
-
|
|
481
|
-
|
|
499
|
+
/**
|
|
500
|
+
* A reference to another token: `token(colors.red.300)`.
|
|
501
|
+
*
|
|
502
|
+
* Deliberately a copy of the regex in `@bamboocss/token-dictionary`, which this package does not
|
|
503
|
+
* depend on. The two must agree: validation is what reports a missing or circular reference, so a
|
|
504
|
+
* spelling only the dictionary understands is one this never checks — which is silence, not an
|
|
505
|
+
* error, and exactly what a spelling change here is most likely to cause.
|
|
506
|
+
*/
|
|
507
|
+
const REFERENCE_REGEX = /token\(([^(),]+)\)/g;
|
|
482
508
|
const isValidToken = (token) => isObject(token) && Object.hasOwnProperty.call(token, "value");
|
|
483
|
-
const isTokenReference = (value) => typeof value === "string" &&
|
|
509
|
+
const isTokenReference = (value) => typeof value === "string" && getReferences(value).length > 0;
|
|
510
|
+
/**
|
|
511
|
+
* The retired curly reference — `{colors.red.300}`, or `{$spacing-2}` under a custom
|
|
512
|
+
* `formatTokenName`. A copy of the regex in `@bamboocss/token-dictionary`, which this package
|
|
513
|
+
* does not depend on.
|
|
514
|
+
*
|
|
515
|
+
* Reported here as well as there because a *token* value carrying one is the worse case: the
|
|
516
|
+
* text is emitted into the stylesheet rather than dropped, and validation is the only thing that
|
|
517
|
+
* can name which token it came from.
|
|
518
|
+
*/
|
|
519
|
+
const CURLY_REFERENCE = /\{[^{}\s:;"']+\}/;
|
|
520
|
+
const findCurlyReference = (value) => value.includes("{") ? CURLY_REFERENCE.exec(value)?.[0] ?? void 0 : void 0;
|
|
521
|
+
/** The retired `token(path, fallback)` form. See `findCurlyReference` for why these fail. */
|
|
522
|
+
const FALLBACK_REFERENCE = /token\([^(),]+,[^()]*\)/;
|
|
523
|
+
const findFallbackReference = (value) => value.includes("token(") ? FALLBACK_REFERENCE.exec(value)?.[0] ?? void 0 : void 0;
|
|
484
524
|
const formatPath = (path) => path;
|
|
485
525
|
function getReferences(value) {
|
|
486
526
|
if (typeof value !== "string") return [];
|
|
487
|
-
|
|
488
|
-
if (!matches) return [];
|
|
489
|
-
return matches.map((match) => match.replace(curlyBracketRegex, "")).map((value) => {
|
|
490
|
-
return value.trim().split("/")[0];
|
|
491
|
-
});
|
|
527
|
+
return [...value.matchAll(REFERENCE_REGEX)].map((match) => match[1].trim().split("/")[0]).filter(Boolean);
|
|
492
528
|
}
|
|
493
529
|
const serializeTokenValue = (value) => {
|
|
494
530
|
if (isString(value)) return value;
|
|
@@ -545,6 +581,46 @@ const tokenKeys = [
|
|
|
545
581
|
"deprecated"
|
|
546
582
|
];
|
|
547
583
|
/**
|
|
584
|
+
* Options whose scalar form is shorthand for setting every member of their object form.
|
|
585
|
+
*
|
|
586
|
+
* `hash: true` says both `cssVar` and `className`; `prefix: 'bb'` says both; `preflight: true`
|
|
587
|
+
* says "on, with the defaults". Expanding them is what lets the object forms compose: a preset
|
|
588
|
+
* that sets `prefix.className` and an app that sets `prefix.cssVar` should end up with both,
|
|
589
|
+
* and before this the app's object replaced the preset's wholesale — silently, since the two
|
|
590
|
+
* name different members. `hash`'s members are optional, so writing the partial form that
|
|
591
|
+
* triggered it is the natural thing to do.
|
|
592
|
+
*
|
|
593
|
+
* `preflight: false` has no object form — there is no member meaning "off" — so it stays a
|
|
594
|
+
* scalar and wins outright when it is the value the winning config states.
|
|
595
|
+
*/
|
|
596
|
+
const SCALAR_SHORTHANDS = {
|
|
597
|
+
hash: (value) => typeof value === "boolean" ? {
|
|
598
|
+
cssVar: value,
|
|
599
|
+
className: value
|
|
600
|
+
} : value,
|
|
601
|
+
prefix: (value) => typeof value === "string" ? {
|
|
602
|
+
cssVar: value,
|
|
603
|
+
className: value
|
|
604
|
+
} : value,
|
|
605
|
+
preflight: (value) => value === true ? {} : value
|
|
606
|
+
};
|
|
607
|
+
/**
|
|
608
|
+
* Merge one of those, winner-first per member.
|
|
609
|
+
*
|
|
610
|
+
* `records` arrives in precedence order — the user's config, then each preset — which is the
|
|
611
|
+
* order `assign` wants, since it only fills keys the target does not already have.
|
|
612
|
+
*/
|
|
613
|
+
function mergeScalarShorthand(key, records) {
|
|
614
|
+
const normalize = SCALAR_SHORTHANDS[key];
|
|
615
|
+
const values = records.map((record) => record[key]).filter((value) => value !== void 0);
|
|
616
|
+
if (!values.length) return void 0;
|
|
617
|
+
if (values[0] === false) return false;
|
|
618
|
+
const objects = values.map(normalize).filter((value) => value !== null && typeof value === "object");
|
|
619
|
+
if (!objects.length) return values[0];
|
|
620
|
+
const merged = objects.reduce((acc, object) => assign(acc, object), {});
|
|
621
|
+
return isEmptyObject(merged) ? values[0] : merged;
|
|
622
|
+
}
|
|
623
|
+
/**
|
|
548
624
|
* Merge all configs into a single config
|
|
549
625
|
*/
|
|
550
626
|
function mergeConfigs(configs) {
|
|
@@ -555,17 +631,27 @@ function mergeConfigs(configs) {
|
|
|
555
631
|
hooks: userConfig.hooks
|
|
556
632
|
});
|
|
557
633
|
const reversed = Array.from(configs).reverse();
|
|
634
|
+
const theme = mergeExtensions(reversed.map((config) => config.theme ?? {}));
|
|
635
|
+
const themeVariants = mergeExtensions(reversed.map((config) => config.theme?.variants ?? {}));
|
|
636
|
+
if (isEmptyObject(themeVariants)) delete theme.variants;
|
|
637
|
+
else theme.variants = themeVariants;
|
|
638
|
+
const global = compact({
|
|
639
|
+
css: mergeExtensions(reversed.map((config) => config.global?.css ?? {})),
|
|
640
|
+
vars: mergeExtensions(reversed.map((config) => config.global?.vars ?? {})),
|
|
641
|
+
fontface: mergeExtensions(reversed.map((config) => config.global?.fontface ?? {})),
|
|
642
|
+
positionTry: mergeExtensions(reversed.map((config) => config.global?.positionTry ?? {}))
|
|
643
|
+
});
|
|
558
644
|
const withoutEmpty = compact(assign({
|
|
559
645
|
conditions: mergeExtensions(reversed.map((config) => config.conditions ?? {})),
|
|
560
|
-
theme
|
|
646
|
+
theme,
|
|
561
647
|
patterns: mergeExtensions(reversed.map((config) => config.patterns ?? {})),
|
|
562
648
|
utilities: mergeExtensions(reversed.map((config) => config.utilities ?? {})),
|
|
563
|
-
|
|
564
|
-
globalVars: mergeExtensions(reversed.map((config) => config.globalVars ?? {})),
|
|
565
|
-
globalFontface: mergeExtensions(reversed.map((config) => config.globalFontface ?? {})),
|
|
566
|
-
globalPositionTry: mergeExtensions(reversed.map((config) => config.globalPositionTry ?? {})),
|
|
649
|
+
global,
|
|
567
650
|
staticCss: mergeExtensions(reversed.map((config) => config.staticCss ?? {})),
|
|
568
|
-
|
|
651
|
+
prune: mergeExtensions(reversed.map((config) => config.prune ?? {})),
|
|
652
|
+
hash: mergeScalarShorthand("hash", reversed),
|
|
653
|
+
prefix: mergeScalarShorthand("prefix", reversed),
|
|
654
|
+
preflight: mergeScalarShorthand("preflight", reversed),
|
|
569
655
|
hooks: mergeHooks(pluginHooks)
|
|
570
656
|
}, ...reversed));
|
|
571
657
|
/**
|
|
@@ -651,18 +737,6 @@ async function getResolvedConfig(config, cwd, hooks) {
|
|
|
651
737
|
return merged;
|
|
652
738
|
}
|
|
653
739
|
//#endregion
|
|
654
|
-
//#region src/bundled-preset.ts
|
|
655
|
-
const bundledPresets = {
|
|
656
|
-
"@bamboocss/preset-base": presetBase,
|
|
657
|
-
"@bamboocss/preset-bamboo": presetBamboo,
|
|
658
|
-
"@bamboocss/dev/presets": presetBamboo
|
|
659
|
-
};
|
|
660
|
-
const bundledPresetsNames = Object.keys(bundledPresets);
|
|
661
|
-
const isBundledPreset = (preset) => bundledPresetsNames.includes(preset);
|
|
662
|
-
const getBundledPreset = (preset) => {
|
|
663
|
-
return typeof preset === "string" && isBundledPreset(preset) ? bundledPresets[preset] : void 0;
|
|
664
|
-
};
|
|
665
|
-
//#endregion
|
|
666
740
|
//#region src/validation/validate-artifact.ts
|
|
667
741
|
const validateArtifactNames = (names, addError) => {
|
|
668
742
|
names.recipes.forEach((recipeName) => {
|
|
@@ -757,6 +831,109 @@ const validateRecipes = (options) => {
|
|
|
757
831
|
return artifacts;
|
|
758
832
|
};
|
|
759
833
|
//#endregion
|
|
834
|
+
//#region src/validation/validate-removed.ts
|
|
835
|
+
/**
|
|
836
|
+
* Config options that no longer exist, and what replaced them.
|
|
837
|
+
*
|
|
838
|
+
* An unknown key is otherwise *silently ignored* — nothing walks the config for keys it does not
|
|
839
|
+
* recognise. So removing an option without this leaves the worst possible upgrade: the build
|
|
840
|
+
* reverts to the default and says nothing, and an assertion the user asked for simply stops being
|
|
841
|
+
* enforced. That is exactly the shape a renamed prune flag would have taken.
|
|
842
|
+
*
|
|
843
|
+
* Keyed by the removed name so the message can say what to write instead, rather than reporting a
|
|
844
|
+
* bare "unknown option". Entries can be dropped a release or two after removal, once nobody is
|
|
845
|
+
* upgrading across them.
|
|
846
|
+
*/
|
|
847
|
+
const REMOVED = {
|
|
848
|
+
pruneUnusedTokens: (value) => value === "strict" ? `\`pruneUnusedTokens: 'strict'\` is now \`prune: { tokens: 'accounted', unresolvedPath: 'error' }\`.` : `\`pruneUnusedTokens\` is now \`prune: { tokens: '${value === false ? "off" : "reachable"}' }\`.`,
|
|
849
|
+
pruneUnusedKeyframes: (value) => `\`pruneUnusedKeyframes\` is now \`prune: { keyframes: ${value === false ? "false" : "true"} }\`.`,
|
|
850
|
+
prunePreflight: (value) => `\`prunePreflight\` is now \`prune: { preflight: ${value === false ? "false" : "true"} }\`.`,
|
|
851
|
+
globalCss: () => `\`globalCss\` is now \`global: { css }\`.`,
|
|
852
|
+
globalFontface: () => `\`globalFontface\` is now \`global: { fontface }\`.`,
|
|
853
|
+
globalPositionTry: () => `\`globalPositionTry\` is now \`global: { positionTry }\`.`,
|
|
854
|
+
globalVars: () => `\`globalVars\` is now \`global: { vars }\`.`,
|
|
855
|
+
themes: () => `\`themes\` is now \`theme.variants\`. One character from \`theme\`, both spellings valid, so the typo resolved to a different feature instead of an error.`,
|
|
856
|
+
eject: (value) => value ? `\`eject: true\` is now \`presets: []\`. \`presets\` is the complete list — an unset \`presets\` loads \`defaultPresets\`, and listing your own no longer keeps a default underneath it.` : `\`eject: false\` is the default and can be removed. \`presets\` is now the complete list.`,
|
|
857
|
+
lightningcss: (value) => value ? `\`lightningcss: true\` is now \`plugins: [pluginLightningcss()]\` from \`@bamboocss/plugin-lightningcss\`, which you install yourself. The flag forced a static import, so every project carried the native binary whether or not it was on.` : `\`lightningcss: false\` is the default and can be removed.`
|
|
858
|
+
};
|
|
859
|
+
/** Values that no longer exist for an option that does. Keyed by option, then by old value. */
|
|
860
|
+
const RETIRED_VALUES = { validation: { none: `\`validation: 'none'\` is now \`validation: 'off'\`, matching \`prune.unresolvedPath\`.` } };
|
|
861
|
+
/** Removed keys nested one level down, keyed by their parent. */
|
|
862
|
+
const REMOVED_NESTED = { prune: { unresolved: (value) => `\`prune.unresolved\` is now \`prune.unresolvedPath\`, and the accounting pass it used to switch on is now \`prune.tokens: 'accounted'\` — write \`prune: { tokens: 'accounted', unresolvedPath: '${value === "error" ? "error" : "warn"}' }\`. They are separate because \`'off'\` used to mean two things at once: no accounting, and no report.` } };
|
|
863
|
+
function validateRemovedOptions(config, addError) {
|
|
864
|
+
const dict = config;
|
|
865
|
+
for (const [name, describe] of Object.entries(REMOVED)) {
|
|
866
|
+
if (!Object.hasOwn(config, name)) continue;
|
|
867
|
+
addError("config", describe(dict[name]));
|
|
868
|
+
}
|
|
869
|
+
for (const [parent, removed] of Object.entries(REMOVED_NESTED)) {
|
|
870
|
+
const value = dict[parent];
|
|
871
|
+
if (value == null || typeof value !== "object") continue;
|
|
872
|
+
for (const [name, describe] of Object.entries(removed)) {
|
|
873
|
+
if (!Object.hasOwn(value, name)) continue;
|
|
874
|
+
addError("config", describe(value[name]));
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
for (const [name, retired] of Object.entries(RETIRED_VALUES)) {
|
|
878
|
+
const value = dict[name];
|
|
879
|
+
if (typeof value !== "string") continue;
|
|
880
|
+
const message = retired[value];
|
|
881
|
+
if (message) addError("config", message);
|
|
882
|
+
}
|
|
883
|
+
if (typeof dict.prune?.tokens === "boolean") addError("config", `\`prune.tokens\` takes a strategy now, not a boolean — write \`'${dict.prune.tokens ? "reachable" : "off"}'\`. \`'accounted'\` is the new one: keeps computed from the token paths in your source rather than from what the css reaches.`);
|
|
884
|
+
for (const [name, pattern] of Object.entries(config.patterns ?? {})) {
|
|
885
|
+
const dictPattern = pattern;
|
|
886
|
+
if (!dictPattern) continue;
|
|
887
|
+
if (Object.hasOwn(dictPattern, "strict")) addError("patterns", `\`${name}.strict\` is now \`${name}.cssProps: '${dictPattern.strict ? "none" : "all"}'\`.`);
|
|
888
|
+
if (Object.hasOwn(dictPattern, "blocklist")) addError("patterns", `\`${name}.blocklist\` is now \`${name}.cssProps: { except: [...] }\`, which is no longer experimental and no longer silently dropped when the pattern also set \`strict\`.`);
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
//#endregion
|
|
892
|
+
//#region src/validation/validate-retired-syntax.ts
|
|
893
|
+
/**
|
|
894
|
+
* Token values still written in a retired reference syntax.
|
|
895
|
+
*
|
|
896
|
+
* A hard error, and one that runs ahead of `validation` rather than under it. The rest of
|
|
897
|
+
* `validateConfig` reports opinions about a config that will still build; this reports a spelling
|
|
898
|
+
* that no longer means anything, whose output is broken either way — in a token value the text is
|
|
899
|
+
* emitted into the stylesheet as-is, and nothing downstream reports it. `validation: 'off'` is an
|
|
900
|
+
* opt-out of opinions, not of that.
|
|
901
|
+
*
|
|
902
|
+
* Safe to throw on because the spelling was never available for anything else: until it was
|
|
903
|
+
* removed, `{…}` in a value was consumed unconditionally — braces stripped, unresolved paths
|
|
904
|
+
* emitted bare — so no literal `{a.b}` could have survived to mean itself.
|
|
905
|
+
*
|
|
906
|
+
* Every occurrence is collected before throwing, because the point is to fix a config once rather
|
|
907
|
+
* than to be told about it one token at a time. Delete this a release or two after removal, along
|
|
908
|
+
* with `validate-removed.ts`.
|
|
909
|
+
*/
|
|
910
|
+
function assertNoRetiredSyntax(config) {
|
|
911
|
+
const found = [];
|
|
912
|
+
const collect = (source, label) => {
|
|
913
|
+
if (!source) return;
|
|
914
|
+
walkObject(source, (token, path) => {
|
|
915
|
+
if (!isValidToken(token)) return;
|
|
916
|
+
const value = serializeTokenValue(token.value ?? token);
|
|
917
|
+
const at = `${label}.${formatPath(path.join("."))}`;
|
|
918
|
+
const curly = findCurlyReference(value);
|
|
919
|
+
if (curly) found.push(`- \`${at}\`: \`${curly}\` → \`token(${curly.slice(1, -1)})\``);
|
|
920
|
+
const fallback = findFallbackReference(value);
|
|
921
|
+
if (fallback) {
|
|
922
|
+
const path = fallback.slice(6, fallback.lastIndexOf(",")).trim();
|
|
923
|
+
found.push(`- \`${at}\`: \`${fallback}\` → \`token(${path})\``);
|
|
924
|
+
}
|
|
925
|
+
}, { stop: isValidToken });
|
|
926
|
+
};
|
|
927
|
+
collect(config.theme?.tokens, "theme.tokens");
|
|
928
|
+
collect(config.theme?.semanticTokens, "theme.semanticTokens");
|
|
929
|
+
for (const [name, variant] of Object.entries(config.theme?.variants ?? {})) {
|
|
930
|
+
collect(variant?.tokens, `theme.variants.${name}.tokens`);
|
|
931
|
+
collect(variant?.semanticTokens, `theme.variants.${name}.semanticTokens`);
|
|
932
|
+
}
|
|
933
|
+
if (!found.length) return;
|
|
934
|
+
throw new BambooError("CONFIG_ERROR", `${found.length} token value(s) use a retired reference syntax:\n\n${found.join("\n")}\n\n\`{colors.red.300}\` and \`token(colors.red.300, fallback)\` were both removed so a token is referenced one way. Neither is ignored quietly — the text is emitted into the stylesheet as-is.`);
|
|
935
|
+
}
|
|
936
|
+
//#endregion
|
|
760
937
|
//#region src/validation/validate-token-references.ts
|
|
761
938
|
const validateTokenReferences = (props) => {
|
|
762
939
|
const { valueAtPath, refsByPath, addError, typeByPath } = props;
|
|
@@ -877,13 +1054,24 @@ const validateTokens = (options) => {
|
|
|
877
1054
|
* - Check for missing tokens references
|
|
878
1055
|
* - Check for conditions selectors (must contain '&')
|
|
879
1056
|
* - Check for breakpoints units (must be the same)
|
|
1057
|
+
* - Check for options that have been removed, which are otherwise ignored in silence
|
|
1058
|
+
* - Throw on token values still written in the retired curly reference syntax
|
|
880
1059
|
*/
|
|
881
1060
|
const validateConfig = (config) => {
|
|
882
|
-
|
|
1061
|
+
assertNoRetiredSyntax(config);
|
|
883
1062
|
const warnings = /* @__PURE__ */ new Set();
|
|
884
1063
|
const addError = (scope, message) => {
|
|
885
1064
|
warnings.add(`[${scope}] ` + message);
|
|
886
1065
|
};
|
|
1066
|
+
validateRemovedOptions(config, addError);
|
|
1067
|
+
const report = () => {
|
|
1068
|
+
if (!warnings.size) return;
|
|
1069
|
+
const errors = `⚠️ Invalid config:\n${Array.from(warnings).map((err) => "- " + err).join("\n")}\n`;
|
|
1070
|
+
if (config.validation === "error") throw new BambooError("CONFIG_ERROR", errors);
|
|
1071
|
+
logger.warn("config", errors);
|
|
1072
|
+
return warnings;
|
|
1073
|
+
};
|
|
1074
|
+
if (config.validation === "off") return report();
|
|
887
1075
|
validateBreakpoints(config.theme?.breakpoints, addError);
|
|
888
1076
|
validateConditions(config.conditions, addError);
|
|
889
1077
|
const artifacts = {
|
|
@@ -913,12 +1101,7 @@ const validateConfig = (config) => {
|
|
|
913
1101
|
}
|
|
914
1102
|
validatePatterns(config.patterns, artifacts);
|
|
915
1103
|
validateArtifactNames(artifacts, addError);
|
|
916
|
-
|
|
917
|
-
const errors = `⚠️ Invalid config:\n${Array.from(warnings).map((err) => "- " + err).join("\n")}\n`;
|
|
918
|
-
if (config.validation === "error") throw new BambooError("CONFIG_ERROR", errors);
|
|
919
|
-
logger.warn("config", errors);
|
|
920
|
-
return warnings;
|
|
921
|
-
}
|
|
1104
|
+
return report();
|
|
922
1105
|
};
|
|
923
1106
|
//#endregion
|
|
924
1107
|
//#region src/resolve-config.ts
|
|
@@ -928,18 +1111,37 @@ const hookUtils = {
|
|
|
928
1111
|
traverse
|
|
929
1112
|
};
|
|
930
1113
|
/**
|
|
931
|
-
*
|
|
932
|
-
*
|
|
933
|
-
*
|
|
1114
|
+
* The one way this rename can break a config without saying so.
|
|
1115
|
+
*
|
|
1116
|
+
* `presets` still exists and still takes a list, so nothing in `validate-removed` notices
|
|
1117
|
+
* that its meaning changed. A config that listed `[myPreset]` used to get `preset-base`
|
|
1118
|
+
* underneath it and now does not — and what `preset-base` carries is the utility table, so
|
|
1119
|
+
* the failure is every class name silently changing (`c_red_300` becomes `color_red_300`)
|
|
1120
|
+
* rather than an error. That is the shape this codebase treats as the worst upgrade there
|
|
1121
|
+
* is, so it gets a message.
|
|
1122
|
+
*
|
|
1123
|
+
* Skipped for an empty list, which is a deliberate eject and the replacement for
|
|
1124
|
+
* `eject: true`. Drop this a release or two after the rename.
|
|
1125
|
+
*/
|
|
1126
|
+
function warnIfBaseDropped(listed, resolved) {
|
|
1127
|
+
if (!listed?.length) return;
|
|
1128
|
+
if (resolved.some((preset) => preset?.name === "@bamboocss/preset-base")) return;
|
|
1129
|
+
logger.warn("config", "`presets` is now the complete list, and this one does not include `@bamboocss/preset-base` — so its utilities, conditions and patterns are not loaded, and generated class names change. Listing a preset used to keep `preset-base` underneath it.\n\n import { defaultPresets } from '@bamboocss/dev/presets'\n presets: [...defaultPresets, yourPreset]\n\nIf dropping it is deliberate, this is the intended behaviour and the warning goes away once `preset-base` is listed explicitly.");
|
|
1130
|
+
}
|
|
1131
|
+
/**
|
|
1132
|
+
* Resolve the final config (including presets).
|
|
1133
|
+
*
|
|
1134
|
+
* `presets` is authoritative: what the config lists is what is loaded, and an unset
|
|
1135
|
+
* `presets` loads `defaultPresets`. There is no implicit preset a listed one sits on top
|
|
1136
|
+
* of — `eject` used to control that, badly. Under it, listing any preset kept
|
|
1137
|
+
* `@bamboocss/preset-base` and silently dropped `@bamboocss/preset-bamboo`, so `presets`
|
|
1138
|
+
* was neither additive nor replacing, and `presets: []` meant "base only" rather than
|
|
1139
|
+
* "none". Both of those had to be discovered by reading this function.
|
|
934
1140
|
*/
|
|
935
1141
|
async function resolveConfig(result, cwd) {
|
|
936
|
-
const
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
presets.add(getBundledPreset(preset) ?? preset);
|
|
940
|
-
});
|
|
941
|
-
else if (!result.config.eject) presets.add(presetBamboo);
|
|
942
|
-
result.config.presets = Array.from(presets);
|
|
1142
|
+
const listed = result.config.presets;
|
|
1143
|
+
result.config.presets = listed ? Array.from(new Set(listed.map((preset) => getBundledPreset(preset) ?? preset))) : [...defaultPresets];
|
|
1144
|
+
warnIfBaseDropped(listed, result.config.presets);
|
|
943
1145
|
const userConfig = result.config;
|
|
944
1146
|
const pluginHooks = userConfig.plugins ?? [];
|
|
945
1147
|
if (userConfig.hooks) pluginHooks.push({
|
|
@@ -950,6 +1152,7 @@ async function resolveConfig(result, cwd) {
|
|
|
950
1152
|
const mergedConfig = await getResolvedConfig(result.config, cwd, earlyHooks);
|
|
951
1153
|
const hooks = mergedConfig.hooks ?? {};
|
|
952
1154
|
if (mergedConfig.logLevel) logger.level = mergedConfig.logLevel;
|
|
1155
|
+
if (mergedConfig.logFilter) logger.filter = mergedConfig.logFilter;
|
|
953
1156
|
validateConfig(mergedConfig);
|
|
954
1157
|
const loadConfigResult = {
|
|
955
1158
|
...result,
|
|
@@ -985,4 +1188,4 @@ async function loadConfig(options) {
|
|
|
985
1188
|
return resolveConfig(await bundleConfig(options), options.cwd);
|
|
986
1189
|
}
|
|
987
1190
|
//#endregion
|
|
988
|
-
export { bundleConfig, convertTsPathsToRegexes, diffConfigs, findConfig, getConfigDependencies, getResolvedConfig, loadConfig, mergeConfigs, mergeHooks, resolveConfig };
|
|
1191
|
+
export { bundleConfig, convertTsPathsToRegexes, defaultPresets, diffConfigs, findConfig, getConfigDependencies, getResolvedConfig, loadConfig, mergeConfigs, mergeHooks, presetBamboo, presetBase, resolveConfig };
|
package/dist/merge-config.cjs
CHANGED
|
@@ -172,6 +172,46 @@ const tokenKeys = [
|
|
|
172
172
|
"deprecated"
|
|
173
173
|
];
|
|
174
174
|
/**
|
|
175
|
+
* Options whose scalar form is shorthand for setting every member of their object form.
|
|
176
|
+
*
|
|
177
|
+
* `hash: true` says both `cssVar` and `className`; `prefix: 'bb'` says both; `preflight: true`
|
|
178
|
+
* says "on, with the defaults". Expanding them is what lets the object forms compose: a preset
|
|
179
|
+
* that sets `prefix.className` and an app that sets `prefix.cssVar` should end up with both,
|
|
180
|
+
* and before this the app's object replaced the preset's wholesale — silently, since the two
|
|
181
|
+
* name different members. `hash`'s members are optional, so writing the partial form that
|
|
182
|
+
* triggered it is the natural thing to do.
|
|
183
|
+
*
|
|
184
|
+
* `preflight: false` has no object form — there is no member meaning "off" — so it stays a
|
|
185
|
+
* scalar and wins outright when it is the value the winning config states.
|
|
186
|
+
*/
|
|
187
|
+
const SCALAR_SHORTHANDS = {
|
|
188
|
+
hash: (value) => typeof value === "boolean" ? {
|
|
189
|
+
cssVar: value,
|
|
190
|
+
className: value
|
|
191
|
+
} : value,
|
|
192
|
+
prefix: (value) => typeof value === "string" ? {
|
|
193
|
+
cssVar: value,
|
|
194
|
+
className: value
|
|
195
|
+
} : value,
|
|
196
|
+
preflight: (value) => value === true ? {} : value
|
|
197
|
+
};
|
|
198
|
+
/**
|
|
199
|
+
* Merge one of those, winner-first per member.
|
|
200
|
+
*
|
|
201
|
+
* `records` arrives in precedence order — the user's config, then each preset — which is the
|
|
202
|
+
* order `assign` wants, since it only fills keys the target does not already have.
|
|
203
|
+
*/
|
|
204
|
+
function mergeScalarShorthand(key, records) {
|
|
205
|
+
const normalize = SCALAR_SHORTHANDS[key];
|
|
206
|
+
const values = records.map((record) => record[key]).filter((value) => value !== void 0);
|
|
207
|
+
if (!values.length) return void 0;
|
|
208
|
+
if (values[0] === false) return false;
|
|
209
|
+
const objects = values.map(normalize).filter((value) => value !== null && typeof value === "object");
|
|
210
|
+
if (!objects.length) return values[0];
|
|
211
|
+
const merged = objects.reduce((acc, object) => (0, _bamboocss_shared.assign)(acc, object), {});
|
|
212
|
+
return isEmptyObject(merged) ? values[0] : merged;
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
175
215
|
* Merge all configs into a single config
|
|
176
216
|
*/
|
|
177
217
|
function mergeConfigs(configs) {
|
|
@@ -182,17 +222,27 @@ function mergeConfigs(configs) {
|
|
|
182
222
|
hooks: userConfig.hooks
|
|
183
223
|
});
|
|
184
224
|
const reversed = Array.from(configs).reverse();
|
|
225
|
+
const theme = mergeExtensions(reversed.map((config) => config.theme ?? {}));
|
|
226
|
+
const themeVariants = mergeExtensions(reversed.map((config) => config.theme?.variants ?? {}));
|
|
227
|
+
if (isEmptyObject(themeVariants)) delete theme.variants;
|
|
228
|
+
else theme.variants = themeVariants;
|
|
229
|
+
const global = compact({
|
|
230
|
+
css: mergeExtensions(reversed.map((config) => config.global?.css ?? {})),
|
|
231
|
+
vars: mergeExtensions(reversed.map((config) => config.global?.vars ?? {})),
|
|
232
|
+
fontface: mergeExtensions(reversed.map((config) => config.global?.fontface ?? {})),
|
|
233
|
+
positionTry: mergeExtensions(reversed.map((config) => config.global?.positionTry ?? {}))
|
|
234
|
+
});
|
|
185
235
|
const withoutEmpty = compact((0, _bamboocss_shared.assign)({
|
|
186
236
|
conditions: mergeExtensions(reversed.map((config) => config.conditions ?? {})),
|
|
187
|
-
theme
|
|
237
|
+
theme,
|
|
188
238
|
patterns: mergeExtensions(reversed.map((config) => config.patterns ?? {})),
|
|
189
239
|
utilities: mergeExtensions(reversed.map((config) => config.utilities ?? {})),
|
|
190
|
-
|
|
191
|
-
globalVars: mergeExtensions(reversed.map((config) => config.globalVars ?? {})),
|
|
192
|
-
globalFontface: mergeExtensions(reversed.map((config) => config.globalFontface ?? {})),
|
|
193
|
-
globalPositionTry: mergeExtensions(reversed.map((config) => config.globalPositionTry ?? {})),
|
|
240
|
+
global,
|
|
194
241
|
staticCss: mergeExtensions(reversed.map((config) => config.staticCss ?? {})),
|
|
195
|
-
|
|
242
|
+
prune: mergeExtensions(reversed.map((config) => config.prune ?? {})),
|
|
243
|
+
hash: mergeScalarShorthand("hash", reversed),
|
|
244
|
+
prefix: mergeScalarShorthand("prefix", reversed),
|
|
245
|
+
preflight: mergeScalarShorthand("preflight", reversed),
|
|
196
246
|
hooks: mergeHooks(pluginHooks)
|
|
197
247
|
}, ...reversed));
|
|
198
248
|
/**
|
package/dist/merge-config.mjs
CHANGED
|
@@ -171,6 +171,46 @@ const tokenKeys = [
|
|
|
171
171
|
"deprecated"
|
|
172
172
|
];
|
|
173
173
|
/**
|
|
174
|
+
* Options whose scalar form is shorthand for setting every member of their object form.
|
|
175
|
+
*
|
|
176
|
+
* `hash: true` says both `cssVar` and `className`; `prefix: 'bb'` says both; `preflight: true`
|
|
177
|
+
* says "on, with the defaults". Expanding them is what lets the object forms compose: a preset
|
|
178
|
+
* that sets `prefix.className` and an app that sets `prefix.cssVar` should end up with both,
|
|
179
|
+
* and before this the app's object replaced the preset's wholesale — silently, since the two
|
|
180
|
+
* name different members. `hash`'s members are optional, so writing the partial form that
|
|
181
|
+
* triggered it is the natural thing to do.
|
|
182
|
+
*
|
|
183
|
+
* `preflight: false` has no object form — there is no member meaning "off" — so it stays a
|
|
184
|
+
* scalar and wins outright when it is the value the winning config states.
|
|
185
|
+
*/
|
|
186
|
+
const SCALAR_SHORTHANDS = {
|
|
187
|
+
hash: (value) => typeof value === "boolean" ? {
|
|
188
|
+
cssVar: value,
|
|
189
|
+
className: value
|
|
190
|
+
} : value,
|
|
191
|
+
prefix: (value) => typeof value === "string" ? {
|
|
192
|
+
cssVar: value,
|
|
193
|
+
className: value
|
|
194
|
+
} : value,
|
|
195
|
+
preflight: (value) => value === true ? {} : value
|
|
196
|
+
};
|
|
197
|
+
/**
|
|
198
|
+
* Merge one of those, winner-first per member.
|
|
199
|
+
*
|
|
200
|
+
* `records` arrives in precedence order — the user's config, then each preset — which is the
|
|
201
|
+
* order `assign` wants, since it only fills keys the target does not already have.
|
|
202
|
+
*/
|
|
203
|
+
function mergeScalarShorthand(key, records) {
|
|
204
|
+
const normalize = SCALAR_SHORTHANDS[key];
|
|
205
|
+
const values = records.map((record) => record[key]).filter((value) => value !== void 0);
|
|
206
|
+
if (!values.length) return void 0;
|
|
207
|
+
if (values[0] === false) return false;
|
|
208
|
+
const objects = values.map(normalize).filter((value) => value !== null && typeof value === "object");
|
|
209
|
+
if (!objects.length) return values[0];
|
|
210
|
+
const merged = objects.reduce((acc, object) => assign(acc, object), {});
|
|
211
|
+
return isEmptyObject(merged) ? values[0] : merged;
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
174
214
|
* Merge all configs into a single config
|
|
175
215
|
*/
|
|
176
216
|
function mergeConfigs(configs) {
|
|
@@ -181,17 +221,27 @@ function mergeConfigs(configs) {
|
|
|
181
221
|
hooks: userConfig.hooks
|
|
182
222
|
});
|
|
183
223
|
const reversed = Array.from(configs).reverse();
|
|
224
|
+
const theme = mergeExtensions(reversed.map((config) => config.theme ?? {}));
|
|
225
|
+
const themeVariants = mergeExtensions(reversed.map((config) => config.theme?.variants ?? {}));
|
|
226
|
+
if (isEmptyObject(themeVariants)) delete theme.variants;
|
|
227
|
+
else theme.variants = themeVariants;
|
|
228
|
+
const global = compact({
|
|
229
|
+
css: mergeExtensions(reversed.map((config) => config.global?.css ?? {})),
|
|
230
|
+
vars: mergeExtensions(reversed.map((config) => config.global?.vars ?? {})),
|
|
231
|
+
fontface: mergeExtensions(reversed.map((config) => config.global?.fontface ?? {})),
|
|
232
|
+
positionTry: mergeExtensions(reversed.map((config) => config.global?.positionTry ?? {}))
|
|
233
|
+
});
|
|
184
234
|
const withoutEmpty = compact(assign({
|
|
185
235
|
conditions: mergeExtensions(reversed.map((config) => config.conditions ?? {})),
|
|
186
|
-
theme
|
|
236
|
+
theme,
|
|
187
237
|
patterns: mergeExtensions(reversed.map((config) => config.patterns ?? {})),
|
|
188
238
|
utilities: mergeExtensions(reversed.map((config) => config.utilities ?? {})),
|
|
189
|
-
|
|
190
|
-
globalVars: mergeExtensions(reversed.map((config) => config.globalVars ?? {})),
|
|
191
|
-
globalFontface: mergeExtensions(reversed.map((config) => config.globalFontface ?? {})),
|
|
192
|
-
globalPositionTry: mergeExtensions(reversed.map((config) => config.globalPositionTry ?? {})),
|
|
239
|
+
global,
|
|
193
240
|
staticCss: mergeExtensions(reversed.map((config) => config.staticCss ?? {})),
|
|
194
|
-
|
|
241
|
+
prune: mergeExtensions(reversed.map((config) => config.prune ?? {})),
|
|
242
|
+
hash: mergeScalarShorthand("hash", reversed),
|
|
243
|
+
prefix: mergeScalarShorthand("prefix", reversed),
|
|
244
|
+
preflight: mergeScalarShorthand("preflight", reversed),
|
|
195
245
|
hooks: mergeHooks(pluginHooks)
|
|
196
246
|
}, ...reversed));
|
|
197
247
|
/**
|
package/package.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bamboocss/config",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.31.0",
|
|
4
4
|
"description": "Find and load bamboo config",
|
|
5
5
|
"homepage": "https://bamboocss.com",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"author": "Gajus Kuizinas <gajus@gajus.com>",
|
|
8
8
|
"repository": {
|
|
9
9
|
"type": "git",
|
|
10
|
-
"url": "git+https://github.com/bamboocss
|
|
10
|
+
"url": "git+https://github.com/gajus/bamboocss.git",
|
|
11
11
|
"directory": "packages/config"
|
|
12
12
|
},
|
|
13
13
|
"files": [
|
|
@@ -64,11 +64,11 @@
|
|
|
64
64
|
"escalade": "3.2.0",
|
|
65
65
|
"microdiff": "1.5.0",
|
|
66
66
|
"typescript": "6.0.2",
|
|
67
|
-
"@bamboocss/logger": "1.
|
|
68
|
-
"@bamboocss/preset-bamboo": "1.
|
|
69
|
-
"@bamboocss/preset-base": "1.
|
|
70
|
-
"@bamboocss/shared": "1.
|
|
71
|
-
"@bamboocss/types": "1.
|
|
67
|
+
"@bamboocss/logger": "1.31.0",
|
|
68
|
+
"@bamboocss/preset-bamboo": "1.31.0",
|
|
69
|
+
"@bamboocss/preset-base": "1.31.0",
|
|
70
|
+
"@bamboocss/shared": "1.31.0",
|
|
71
|
+
"@bamboocss/types": "1.31.0"
|
|
72
72
|
},
|
|
73
73
|
"devDependencies": {
|
|
74
74
|
"pkg-types": "2.3.0"
|