@foldcn/registry 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/componentsConfig/componentsConfig.d.ts +32 -0
- package/dist/componentsConfig/componentsConfig.d.ts.map +1 -0
- package/dist/componentsConfig/componentsConfig.js +29 -0
- package/dist/componentsConfig/index.d.ts +2 -0
- package/dist/componentsConfig/index.d.ts.map +1 -0
- package/dist/componentsConfig/index.js +1 -0
- package/dist/cssMerge/cssMerge.d.ts +25 -0
- package/dist/cssMerge/cssMerge.d.ts.map +1 -0
- package/dist/cssMerge/cssMerge.js +297 -0
- package/dist/cssMerge/index.d.ts +2 -0
- package/dist/cssMerge/index.d.ts.map +1 -0
- package/dist/cssMerge/index.js +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +4 -0
- package/dist/registry/index.d.ts +2 -0
- package/dist/registry/index.d.ts.map +1 -0
- package/dist/registry/index.js +1 -0
- package/dist/registry/registry.d.ts +101 -0
- package/dist/registry/registry.d.ts.map +1 -0
- package/dist/registry/registry.js +63 -0
- package/dist/resolveTree/index.d.ts +2 -0
- package/dist/resolveTree/index.d.ts.map +1 -0
- package/dist/resolveTree/index.js +1 -0
- package/dist/resolveTree/resolveTree.d.ts +39 -0
- package/dist/resolveTree/resolveTree.d.ts.map +1 -0
- package/dist/resolveTree/resolveTree.js +151 -0
- package/package.json +34 -0
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { Schema } from 'effect';
|
|
2
|
+
/**
|
|
3
|
+
* Directory aliases for where copied files land in a consumer project,
|
|
4
|
+
* relative to the project root.
|
|
5
|
+
*/
|
|
6
|
+
export declare const Aliases: Schema.Struct<{
|
|
7
|
+
readonly ui: Schema.String;
|
|
8
|
+
readonly lib: Schema.String;
|
|
9
|
+
}>;
|
|
10
|
+
export type Aliases = typeof Aliases.Type;
|
|
11
|
+
/**
|
|
12
|
+
* The `components.json` a consumer project carries. `registries` maps
|
|
13
|
+
* `@namespace` keys to URL templates containing `{name}`.
|
|
14
|
+
*/
|
|
15
|
+
export declare const Config: Schema.Struct<{
|
|
16
|
+
readonly $schema: Schema.optionalKey<Schema.String>;
|
|
17
|
+
readonly css: Schema.String;
|
|
18
|
+
readonly baseColor: Schema.String;
|
|
19
|
+
readonly aliases: Schema.Struct<{
|
|
20
|
+
readonly ui: Schema.String;
|
|
21
|
+
readonly lib: Schema.String;
|
|
22
|
+
}>;
|
|
23
|
+
readonly registries: Schema.optionalKey<Schema.$Record<Schema.String, Schema.String>>;
|
|
24
|
+
}>;
|
|
25
|
+
export type Config = typeof Config.Type;
|
|
26
|
+
/** Default stylesheet location, matching create-foldkit-app's template. */
|
|
27
|
+
export declare const defaultCss = "src/styles.css";
|
|
28
|
+
/** Default base color shipped by the foldcn registry. */
|
|
29
|
+
export declare const defaultBaseColor = "neutral";
|
|
30
|
+
/** Default alias directories for copied files. */
|
|
31
|
+
export declare const defaultAliases: Aliases;
|
|
32
|
+
//# sourceMappingURL=componentsConfig.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"componentsConfig.d.ts","sourceRoot":"","sources":["../../src/componentsConfig/componentsConfig.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAA;AAE/B;;;GAGG;AACH,eAAO,MAAM,OAAO;;;EAG0C,CAAA;AAC9D,MAAM,MAAM,OAAO,GAAG,OAAO,OAAO,CAAC,IAAI,CAAA;AAEzC;;;GAGG;AACH,eAAO,MAAM,MAAM;;;;;;;;;EAM0C,CAAA;AAC7D,MAAM,MAAM,MAAM,GAAG,OAAO,MAAM,CAAC,IAAI,CAAA;AAEvC,2EAA2E;AAC3E,eAAO,MAAM,UAAU,mBAAmB,CAAA;AAE1C,yDAAyD;AACzD,eAAO,MAAM,gBAAgB,YAAY,CAAA;AAEzC,kDAAkD;AAClD,eAAO,MAAM,cAAc,EAAE,OAG5B,CAAA"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { Schema } from 'effect';
|
|
2
|
+
/**
|
|
3
|
+
* Directory aliases for where copied files land in a consumer project,
|
|
4
|
+
* relative to the project root.
|
|
5
|
+
*/
|
|
6
|
+
export const Aliases = Schema.Struct({
|
|
7
|
+
ui: Schema.String,
|
|
8
|
+
lib: Schema.String,
|
|
9
|
+
}).annotate({ identifier: 'Foldcn.ComponentsConfig.Aliases' });
|
|
10
|
+
/**
|
|
11
|
+
* The `components.json` a consumer project carries. `registries` maps
|
|
12
|
+
* `@namespace` keys to URL templates containing `{name}`.
|
|
13
|
+
*/
|
|
14
|
+
export const Config = Schema.Struct({
|
|
15
|
+
$schema: Schema.optionalKey(Schema.String),
|
|
16
|
+
css: Schema.String,
|
|
17
|
+
baseColor: Schema.String,
|
|
18
|
+
aliases: Aliases,
|
|
19
|
+
registries: Schema.optionalKey(Schema.Record(Schema.String, Schema.String)),
|
|
20
|
+
}).annotate({ identifier: 'Foldcn.ComponentsConfig.Config' });
|
|
21
|
+
/** Default stylesheet location, matching create-foldkit-app's template. */
|
|
22
|
+
export const defaultCss = 'src/styles.css';
|
|
23
|
+
/** Default base color shipped by the foldcn registry. */
|
|
24
|
+
export const defaultBaseColor = 'neutral';
|
|
25
|
+
/** Default alias directories for copied files. */
|
|
26
|
+
export const defaultAliases = {
|
|
27
|
+
ui: 'src/components/ui',
|
|
28
|
+
lib: 'src/lib',
|
|
29
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/componentsConfig/index.ts"],"names":[],"mappings":"AAAA,cAAc,oBAAoB,CAAA"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './componentsConfig';
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { Effect, Schema } from 'effect';
|
|
2
|
+
import type { Css, CssVars } from '../registry';
|
|
3
|
+
declare const CssParseError_base: Schema.Class<CssParseError, Schema.TaggedStruct<"CssParseError", {
|
|
4
|
+
readonly message: Schema.String;
|
|
5
|
+
}>, import("effect/Cause").YieldableError>;
|
|
6
|
+
/** Raised when the consumer stylesheet cannot be parsed or transformed. */
|
|
7
|
+
export declare class CssParseError extends CssParseError_base {
|
|
8
|
+
}
|
|
9
|
+
/** Options for {@link merge}. */
|
|
10
|
+
export interface MergeOptions {
|
|
11
|
+
readonly css: string;
|
|
12
|
+
readonly cssVars?: CssVars;
|
|
13
|
+
readonly extraCss?: Css;
|
|
14
|
+
readonly isOverwriting?: boolean;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Merges registry cssVars and css into a Tailwind v4 stylesheet. Upserts the
|
|
18
|
+
* dark custom variant, `:root` and `.dark` variable blocks, the `@theme
|
|
19
|
+
* inline` token mapping, and any extra rules. Idempotent: re-running with the
|
|
20
|
+
* same inputs never duplicates blocks, and user content outside touched
|
|
21
|
+
* declarations is preserved.
|
|
22
|
+
*/
|
|
23
|
+
export declare const merge: (options: MergeOptions) => Effect.Effect<string, CssParseError>;
|
|
24
|
+
export {};
|
|
25
|
+
//# sourceMappingURL=cssMerge.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cssMerge.d.ts","sourceRoot":"","sources":["../../src/cssMerge/cssMerge.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAA;AAIvC,OAAO,KAAK,EAAE,GAAG,EAAE,OAAO,EAAE,MAAM,aAAa,CAAA;;;;AAE/C,2EAA2E;AAC3E,qBAAa,aAAc,SAAQ,kBAEjC;CAAG;AAEL,iCAAiC;AACjC,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAA;IACpB,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,CAAA;IAC1B,QAAQ,CAAC,QAAQ,CAAC,EAAE,GAAG,CAAA;IACvB,QAAQ,CAAC,aAAa,CAAC,EAAE,OAAO,CAAA;CACjC;AAWD;;;;;;GAMG;AACH,eAAO,MAAM,KAAK,GAAI,SAAS,YAAY,KAAG,MAAM,CAAC,MAAM,CAAC,MAAM,EAAE,aAAa,CAI7E,CAAA"}
|
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
import { Effect, Schema } from 'effect';
|
|
2
|
+
import postcss from 'postcss';
|
|
3
|
+
/** Raised when the consumer stylesheet cannot be parsed or transformed. */
|
|
4
|
+
export class CssParseError extends Schema.TaggedErrorClass()('CssParseError', {
|
|
5
|
+
message: Schema.String,
|
|
6
|
+
}) {
|
|
7
|
+
}
|
|
8
|
+
const darkVariantParams = '(&:is(.dark *))';
|
|
9
|
+
const radiusScale = {
|
|
10
|
+
'--radius-sm': 'calc(var(--radius) - 4px)',
|
|
11
|
+
'--radius-md': 'calc(var(--radius) - 2px)',
|
|
12
|
+
'--radius-lg': 'var(--radius)',
|
|
13
|
+
'--radius-xl': 'calc(var(--radius) + 4px)',
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* Merges registry cssVars and css into a Tailwind v4 stylesheet. Upserts the
|
|
17
|
+
* dark custom variant, `:root` and `.dark` variable blocks, the `@theme
|
|
18
|
+
* inline` token mapping, and any extra rules. Idempotent: re-running with the
|
|
19
|
+
* same inputs never duplicates blocks, and user content outside touched
|
|
20
|
+
* declarations is preserved.
|
|
21
|
+
*/
|
|
22
|
+
export const merge = (options) => Effect.try({
|
|
23
|
+
try: () => mergeSync(options),
|
|
24
|
+
catch: (error) => new CssParseError({ message: String(error) }),
|
|
25
|
+
});
|
|
26
|
+
const mergeSync = (options) => {
|
|
27
|
+
const root = postcss.parse(options.css);
|
|
28
|
+
const isOverwriting = options.isOverwriting ?? false;
|
|
29
|
+
const hasCssVars = options.cssVars !== undefined &&
|
|
30
|
+
Object.values(options.cssVars).some((vars) => vars !== undefined && Object.keys(vars).length > 0);
|
|
31
|
+
if (hasCssVars && options.cssVars !== undefined) {
|
|
32
|
+
ensureDarkVariant(root);
|
|
33
|
+
applyCssVars(root, options.cssVars, isOverwriting);
|
|
34
|
+
applyThemeMapping(root, options.cssVars);
|
|
35
|
+
}
|
|
36
|
+
if (options.extraCss !== undefined) {
|
|
37
|
+
applyExtraCss(root, options.extraCss);
|
|
38
|
+
}
|
|
39
|
+
let output = root.toResult().css;
|
|
40
|
+
output = output.replace(/(\n\s*\n)+/g, '\n\n');
|
|
41
|
+
return `${output.trimEnd()}\n`;
|
|
42
|
+
};
|
|
43
|
+
const isAtRule = (node, name) => node.type === 'atrule' && node.name === name;
|
|
44
|
+
const findAtRule = (root, name, params) => root.nodes.find((node) => isAtRule(node, name) && (params === undefined || node.params === params));
|
|
45
|
+
const lastAtRule = (root, name) => {
|
|
46
|
+
const matches = root.nodes.filter((node) => isAtRule(node, name));
|
|
47
|
+
return matches.at(-1);
|
|
48
|
+
};
|
|
49
|
+
const ensureDarkVariant = (root) => {
|
|
50
|
+
const existing = root.nodes.find((node) => isAtRule(node, 'custom-variant') && node.params.startsWith('dark'));
|
|
51
|
+
if (existing !== undefined) {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
const variantNode = postcss.atRule({
|
|
55
|
+
name: 'custom-variant',
|
|
56
|
+
params: `dark ${darkVariantParams}`,
|
|
57
|
+
raws: { semicolon: true, before: '\n\n' },
|
|
58
|
+
});
|
|
59
|
+
const lastImport = lastAtRule(root, 'import');
|
|
60
|
+
if (lastImport !== undefined) {
|
|
61
|
+
root.insertAfter(lastImport, variantNode);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
const firstNode = root.nodes.at(0);
|
|
65
|
+
if (firstNode !== undefined) {
|
|
66
|
+
root.insertAfter(firstNode, variantNode);
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
root.append(variantNode);
|
|
70
|
+
};
|
|
71
|
+
const upsertThemeNode = (root) => {
|
|
72
|
+
const existing = findAtRule(root, 'theme', 'inline');
|
|
73
|
+
if (existing !== undefined) {
|
|
74
|
+
return existing;
|
|
75
|
+
}
|
|
76
|
+
const themeNode = postcss.atRule({
|
|
77
|
+
name: 'theme',
|
|
78
|
+
params: 'inline',
|
|
79
|
+
nodes: [],
|
|
80
|
+
raws: { semicolon: true, between: ' ', before: '\n\n' },
|
|
81
|
+
});
|
|
82
|
+
root.append(themeNode);
|
|
83
|
+
return themeNode;
|
|
84
|
+
};
|
|
85
|
+
const upsertRule = (root, selector) => {
|
|
86
|
+
const existing = root.nodes.find((node) => node.type === 'rule' && node.selector === selector);
|
|
87
|
+
if (existing !== undefined) {
|
|
88
|
+
return existing;
|
|
89
|
+
}
|
|
90
|
+
const ruleNode = postcss.rule({
|
|
91
|
+
selector,
|
|
92
|
+
nodes: [],
|
|
93
|
+
raws: { semicolon: true, between: ' ', before: '\n\n' },
|
|
94
|
+
});
|
|
95
|
+
root.append(ruleNode);
|
|
96
|
+
return ruleNode;
|
|
97
|
+
};
|
|
98
|
+
const upsertDeclaration = (container, prop, value, isOverwriting) => {
|
|
99
|
+
const existing = container.nodes?.find((node) => node.type === 'decl' && node.prop === prop);
|
|
100
|
+
const declaration = postcss.decl({ prop, value, raws: { semicolon: true, before: '\n ' } });
|
|
101
|
+
if (existing !== undefined) {
|
|
102
|
+
if (isOverwriting && existing.value !== value) {
|
|
103
|
+
existing.replaceWith(declaration);
|
|
104
|
+
}
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
container.append(declaration);
|
|
108
|
+
};
|
|
109
|
+
const normalizeProp = (name) => `--${name.replace(/^--/, '')}`;
|
|
110
|
+
const applyCssVars = (root, cssVars, isOverwriting) => {
|
|
111
|
+
if (cssVars.theme !== undefined && Object.keys(cssVars.theme).length > 0) {
|
|
112
|
+
const themeNode = upsertThemeNode(root);
|
|
113
|
+
for (const [name, value] of Object.entries(cssVars.theme)) {
|
|
114
|
+
upsertDeclaration(themeNode, normalizeProp(name), value, isOverwriting);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
const selectorVars = [
|
|
118
|
+
[':root', cssVars.light],
|
|
119
|
+
['.dark', cssVars.dark],
|
|
120
|
+
];
|
|
121
|
+
for (const [selector, vars] of selectorVars) {
|
|
122
|
+
if (vars === undefined || Object.keys(vars).length === 0) {
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
const ruleNode = upsertRule(root, selector);
|
|
126
|
+
for (const [name, value] of Object.entries(vars)) {
|
|
127
|
+
upsertDeclaration(ruleNode, normalizeProp(name), value, isOverwriting);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
const isColorValue = (value) => value.startsWith('hsl') ||
|
|
132
|
+
value.startsWith('rgb') ||
|
|
133
|
+
value.startsWith('#') ||
|
|
134
|
+
value.startsWith('oklch') ||
|
|
135
|
+
value.includes('--color-');
|
|
136
|
+
const applyThemeMapping = (root, cssVars) => {
|
|
137
|
+
const scopes = [cssVars.light, cssVars.dark].filter((vars) => vars !== undefined);
|
|
138
|
+
const names = Array.from(new Set(scopes.flatMap((vars) => Object.keys(vars))));
|
|
139
|
+
if (names.length === 0) {
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
const themeNode = upsertThemeNode(root);
|
|
143
|
+
for (const name of names) {
|
|
144
|
+
const value = scopes.map((vars) => vars[name]).find((found) => found !== undefined);
|
|
145
|
+
if (value === undefined) {
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
if (name === 'radius') {
|
|
149
|
+
for (const [prop, radiusValue] of Object.entries(radiusScale)) {
|
|
150
|
+
upsertDeclaration(themeNode, prop, radiusValue, false);
|
|
151
|
+
}
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
const bare = name.replace(/^--/, '');
|
|
155
|
+
const prop = isColorValue(value) ? `--color-${bare}` : `--${bare}`;
|
|
156
|
+
upsertDeclaration(themeNode, prop, `var(--${bare})`, false);
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
const atRuleHeader = (selector) => {
|
|
160
|
+
const match = selector.match(/^@([a-zA-Z-]+)\s*(.*)$/);
|
|
161
|
+
if (match === null) {
|
|
162
|
+
return undefined;
|
|
163
|
+
}
|
|
164
|
+
const name = match[1];
|
|
165
|
+
const params = match[2];
|
|
166
|
+
if (name === undefined) {
|
|
167
|
+
return undefined;
|
|
168
|
+
}
|
|
169
|
+
return [name, params ?? ''];
|
|
170
|
+
};
|
|
171
|
+
const applyExtraCss = (root, extraCss) => {
|
|
172
|
+
for (const [selector, value] of Object.entries(extraCss)) {
|
|
173
|
+
const header = atRuleHeader(selector);
|
|
174
|
+
if (header !== undefined) {
|
|
175
|
+
applyAtRule(root, header[0], header[1], value);
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
if (typeof value === 'object') {
|
|
179
|
+
const ruleNode = upsertRule(root, selector);
|
|
180
|
+
applyBody(ruleNode, value);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
};
|
|
184
|
+
const applyAtRule = (root, name, params, value) => {
|
|
185
|
+
if (name === 'import' || name === 'plugin' || name === 'custom-variant') {
|
|
186
|
+
const exists = root.nodes.some((node) => isAtRule(node, name) && node.params === params);
|
|
187
|
+
if (!exists) {
|
|
188
|
+
const atRuleNode = postcss.atRule({ name, params, raws: { semicolon: true, before: '\n' } });
|
|
189
|
+
const lastImport = lastAtRule(root, 'import');
|
|
190
|
+
if (lastImport !== undefined) {
|
|
191
|
+
root.insertAfter(lastImport, atRuleNode);
|
|
192
|
+
}
|
|
193
|
+
else {
|
|
194
|
+
root.prepend(atRuleNode);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
if (typeof value === 'object' && Object.keys(value).length === 0) {
|
|
200
|
+
const exists = root.nodes.some((node) => isAtRule(node, name) && node.params === params);
|
|
201
|
+
if (!exists) {
|
|
202
|
+
root.append(postcss.atRule({ name, params, raws: { semicolon: true, before: '\n\n' } }));
|
|
203
|
+
}
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
if (name === 'keyframes') {
|
|
207
|
+
const themeNode = upsertThemeNode(root);
|
|
208
|
+
const existing = themeNode.nodes?.find((node) => node.type === 'atrule' && isAtRule(node, 'keyframes') && node.params === params);
|
|
209
|
+
const keyframesNode = postcss.atRule({
|
|
210
|
+
name,
|
|
211
|
+
params,
|
|
212
|
+
nodes: [],
|
|
213
|
+
raws: { semicolon: true, between: ' ', before: '\n ' },
|
|
214
|
+
});
|
|
215
|
+
if (existing !== undefined) {
|
|
216
|
+
existing.replaceWith(keyframesNode);
|
|
217
|
+
}
|
|
218
|
+
else {
|
|
219
|
+
themeNode.append(keyframesNode);
|
|
220
|
+
}
|
|
221
|
+
if (typeof value === 'object') {
|
|
222
|
+
applyBody(keyframesNode, value);
|
|
223
|
+
}
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
const existing = root.nodes.find((node) => isAtRule(node, name) && node.params === params);
|
|
227
|
+
const atRuleNode = existing ??
|
|
228
|
+
(() => {
|
|
229
|
+
const created = postcss.atRule({
|
|
230
|
+
name,
|
|
231
|
+
params,
|
|
232
|
+
nodes: [],
|
|
233
|
+
raws: { semicolon: true, between: ' ', before: '\n\n' },
|
|
234
|
+
});
|
|
235
|
+
root.append(created);
|
|
236
|
+
return created;
|
|
237
|
+
})();
|
|
238
|
+
if (typeof value === 'object') {
|
|
239
|
+
applyBody(atRuleNode, value);
|
|
240
|
+
}
|
|
241
|
+
};
|
|
242
|
+
const applyBody = (container, body) => {
|
|
243
|
+
for (const [key, value] of Object.entries(body)) {
|
|
244
|
+
if (typeof value === 'string' && key.startsWith('@')) {
|
|
245
|
+
const header = atRuleHeader(key);
|
|
246
|
+
if (header !== undefined) {
|
|
247
|
+
const [name, params] = header;
|
|
248
|
+
const exists = container.nodes?.some((node) => node.type === 'atrule' && node.name === name && node.params === params);
|
|
249
|
+
if (exists !== true) {
|
|
250
|
+
container.append(postcss.atRule({ name, params, raws: { semicolon: true, before: '\n ' } }));
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
continue;
|
|
254
|
+
}
|
|
255
|
+
if (typeof value === 'string') {
|
|
256
|
+
upsertDeclaration(container, key, value, true);
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
const header = atRuleHeader(key);
|
|
260
|
+
if (header !== undefined) {
|
|
261
|
+
const [name, params] = header;
|
|
262
|
+
if (typeof value === 'object' && Object.keys(value).length === 0) {
|
|
263
|
+
const exists = container.nodes?.some((node) => node.type === 'atrule' && node.name === name && node.params === params);
|
|
264
|
+
if (exists !== true) {
|
|
265
|
+
container.append(postcss.atRule({ name, params, raws: { semicolon: true, before: '\n ' } }));
|
|
266
|
+
}
|
|
267
|
+
continue;
|
|
268
|
+
}
|
|
269
|
+
const existing = container.nodes?.find((node) => node.type === 'atrule' && node.name === name && node.params === params);
|
|
270
|
+
const nested = existing ??
|
|
271
|
+
(() => {
|
|
272
|
+
const created = postcss.atRule({
|
|
273
|
+
name,
|
|
274
|
+
params,
|
|
275
|
+
nodes: [],
|
|
276
|
+
raws: { semicolon: true, between: ' ', before: '\n ' },
|
|
277
|
+
});
|
|
278
|
+
container.append(created);
|
|
279
|
+
return created;
|
|
280
|
+
})();
|
|
281
|
+
applyBody(nested, value);
|
|
282
|
+
continue;
|
|
283
|
+
}
|
|
284
|
+
const existing = container.nodes?.find((node) => node.type === 'rule' && node.selector === key);
|
|
285
|
+
const nested = existing ??
|
|
286
|
+
(() => {
|
|
287
|
+
const created = postcss.rule({
|
|
288
|
+
selector: key,
|
|
289
|
+
nodes: [],
|
|
290
|
+
raws: { semicolon: true, between: ' ', before: '\n ' },
|
|
291
|
+
});
|
|
292
|
+
container.append(created);
|
|
293
|
+
return created;
|
|
294
|
+
})();
|
|
295
|
+
applyBody(nested, value);
|
|
296
|
+
}
|
|
297
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/cssMerge/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAA"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './cssMerge';
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,QAAQ,MAAM,YAAY,CAAA;AACtC,OAAO,KAAK,gBAAgB,MAAM,oBAAoB,CAAA;AACtD,OAAO,KAAK,WAAW,MAAM,eAAe,CAAA;AAC5C,OAAO,KAAK,QAAQ,MAAM,YAAY,CAAA"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/registry/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAA"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './registry';
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { Schema } from 'effect';
|
|
2
|
+
/**
|
|
3
|
+
* The kind of a registry item or file. Mirrors shadcn's registry item types,
|
|
4
|
+
* trimmed to what FoldKit projects need.
|
|
5
|
+
*/
|
|
6
|
+
export declare const ItemType: Schema.Literals<readonly ["registry:ui", "registry:lib", "registry:block", "registry:theme", "registry:file", "registry:example"]>;
|
|
7
|
+
export type ItemType = typeof ItemType.Type;
|
|
8
|
+
/**
|
|
9
|
+
* A single distributable file within a registry item. `content` is inlined by
|
|
10
|
+
* `foldcn build`; `target` is required for `registry:file` items and resolved
|
|
11
|
+
* against the consumer project root.
|
|
12
|
+
*/
|
|
13
|
+
export declare const ItemFile: Schema.Struct<{
|
|
14
|
+
readonly path: Schema.String;
|
|
15
|
+
readonly type: Schema.Literals<readonly ["registry:ui", "registry:lib", "registry:block", "registry:theme", "registry:file", "registry:example"]>;
|
|
16
|
+
readonly target: Schema.optionalKey<Schema.String>;
|
|
17
|
+
readonly content: Schema.optionalKey<Schema.String>;
|
|
18
|
+
}>;
|
|
19
|
+
export type ItemFile = typeof ItemFile.Type;
|
|
20
|
+
/**
|
|
21
|
+
* CSS variables contributed by an item, merged into the consumer stylesheet.
|
|
22
|
+
* `theme` lands in `@theme inline`, `light` in `:root`, `dark` in `.dark`.
|
|
23
|
+
*/
|
|
24
|
+
export declare const CssVars: Schema.Struct<{
|
|
25
|
+
readonly theme: Schema.optionalKey<Schema.$Record<Schema.String, Schema.String>>;
|
|
26
|
+
readonly light: Schema.optionalKey<Schema.$Record<Schema.String, Schema.String>>;
|
|
27
|
+
readonly dark: Schema.optionalKey<Schema.$Record<Schema.String, Schema.String>>;
|
|
28
|
+
}>;
|
|
29
|
+
export type CssVars = typeof CssVars.Type;
|
|
30
|
+
/**
|
|
31
|
+
* Arbitrary nested CSS contributed by an item, keyed by selector or at-rule.
|
|
32
|
+
* String values are declarations, nested records are rules.
|
|
33
|
+
*/
|
|
34
|
+
export interface Css {
|
|
35
|
+
readonly [selector: string]: string | Css;
|
|
36
|
+
}
|
|
37
|
+
export declare const Css: Schema.$Record<Schema.String, Schema.Union<readonly [Schema.String, Schema.suspend<Schema.Codec<Css, Css, never, never>>]>>;
|
|
38
|
+
/**
|
|
39
|
+
* A registry item: one distributable unit (component, lib file, theme, block,
|
|
40
|
+
* or arbitrary file) with its npm and registry dependencies.
|
|
41
|
+
*/
|
|
42
|
+
export declare const Item: Schema.Struct<{
|
|
43
|
+
readonly $schema: Schema.optionalKey<Schema.String>;
|
|
44
|
+
readonly name: Schema.String;
|
|
45
|
+
readonly type: Schema.Literals<readonly ["registry:ui", "registry:lib", "registry:block", "registry:theme", "registry:file", "registry:example"]>;
|
|
46
|
+
readonly title: Schema.optionalKey<Schema.String>;
|
|
47
|
+
readonly description: Schema.optionalKey<Schema.String>;
|
|
48
|
+
readonly dependencies: Schema.optionalKey<Schema.$Array<Schema.String>>;
|
|
49
|
+
readonly devDependencies: Schema.optionalKey<Schema.$Array<Schema.String>>;
|
|
50
|
+
readonly registryDependencies: Schema.optionalKey<Schema.$Array<Schema.String>>;
|
|
51
|
+
readonly files: Schema.optionalKey<Schema.$Array<Schema.Struct<{
|
|
52
|
+
readonly path: Schema.String;
|
|
53
|
+
readonly type: Schema.Literals<readonly ["registry:ui", "registry:lib", "registry:block", "registry:theme", "registry:file", "registry:example"]>;
|
|
54
|
+
readonly target: Schema.optionalKey<Schema.String>;
|
|
55
|
+
readonly content: Schema.optionalKey<Schema.String>;
|
|
56
|
+
}>>>;
|
|
57
|
+
readonly cssVars: Schema.optionalKey<Schema.Struct<{
|
|
58
|
+
readonly theme: Schema.optionalKey<Schema.$Record<Schema.String, Schema.String>>;
|
|
59
|
+
readonly light: Schema.optionalKey<Schema.$Record<Schema.String, Schema.String>>;
|
|
60
|
+
readonly dark: Schema.optionalKey<Schema.$Record<Schema.String, Schema.String>>;
|
|
61
|
+
}>>;
|
|
62
|
+
readonly css: Schema.optionalKey<Schema.$Record<Schema.String, Schema.Union<readonly [Schema.String, Schema.suspend<Schema.Codec<Css, Css, never, never>>]>>>;
|
|
63
|
+
readonly docs: Schema.optionalKey<Schema.String>;
|
|
64
|
+
readonly categories: Schema.optionalKey<Schema.$Array<Schema.String>>;
|
|
65
|
+
}>;
|
|
66
|
+
export type Item = typeof Item.Type;
|
|
67
|
+
/**
|
|
68
|
+
* The registry manifest (`registry.json`): the list of items a registry
|
|
69
|
+
* publishes, built into per-item JSON by `foldcn build`.
|
|
70
|
+
*/
|
|
71
|
+
export declare const Manifest: Schema.Struct<{
|
|
72
|
+
readonly $schema: Schema.optionalKey<Schema.String>;
|
|
73
|
+
readonly name: Schema.String;
|
|
74
|
+
readonly homepage: Schema.String;
|
|
75
|
+
readonly items: Schema.$Array<Schema.Struct<{
|
|
76
|
+
readonly $schema: Schema.optionalKey<Schema.String>;
|
|
77
|
+
readonly name: Schema.String;
|
|
78
|
+
readonly type: Schema.Literals<readonly ["registry:ui", "registry:lib", "registry:block", "registry:theme", "registry:file", "registry:example"]>;
|
|
79
|
+
readonly title: Schema.optionalKey<Schema.String>;
|
|
80
|
+
readonly description: Schema.optionalKey<Schema.String>;
|
|
81
|
+
readonly dependencies: Schema.optionalKey<Schema.$Array<Schema.String>>;
|
|
82
|
+
readonly devDependencies: Schema.optionalKey<Schema.$Array<Schema.String>>;
|
|
83
|
+
readonly registryDependencies: Schema.optionalKey<Schema.$Array<Schema.String>>;
|
|
84
|
+
readonly files: Schema.optionalKey<Schema.$Array<Schema.Struct<{
|
|
85
|
+
readonly path: Schema.String;
|
|
86
|
+
readonly type: Schema.Literals<readonly ["registry:ui", "registry:lib", "registry:block", "registry:theme", "registry:file", "registry:example"]>;
|
|
87
|
+
readonly target: Schema.optionalKey<Schema.String>;
|
|
88
|
+
readonly content: Schema.optionalKey<Schema.String>;
|
|
89
|
+
}>>>;
|
|
90
|
+
readonly cssVars: Schema.optionalKey<Schema.Struct<{
|
|
91
|
+
readonly theme: Schema.optionalKey<Schema.$Record<Schema.String, Schema.String>>;
|
|
92
|
+
readonly light: Schema.optionalKey<Schema.$Record<Schema.String, Schema.String>>;
|
|
93
|
+
readonly dark: Schema.optionalKey<Schema.$Record<Schema.String, Schema.String>>;
|
|
94
|
+
}>>;
|
|
95
|
+
readonly css: Schema.optionalKey<Schema.$Record<Schema.String, Schema.Union<readonly [Schema.String, Schema.suspend<Schema.Codec<Css, Css, never, never>>]>>>;
|
|
96
|
+
readonly docs: Schema.optionalKey<Schema.String>;
|
|
97
|
+
readonly categories: Schema.optionalKey<Schema.$Array<Schema.String>>;
|
|
98
|
+
}>>;
|
|
99
|
+
}>;
|
|
100
|
+
export type Manifest = typeof Manifest.Type;
|
|
101
|
+
//# sourceMappingURL=registry.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../../src/registry/registry.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAA;AAE/B;;;GAGG;AACH,eAAO,MAAM,QAAQ,oIAOkC,CAAA;AACvD,MAAM,MAAM,QAAQ,GAAG,OAAO,QAAQ,CAAC,IAAI,CAAA;AAE3C;;;;GAIG;AACH,eAAO,MAAM,QAAQ;;;;;EAKkC,CAAA;AACvD,MAAM,MAAM,QAAQ,GAAG,OAAO,QAAQ,CAAC,IAAI,CAAA;AAE3C;;;GAGG;AACH,eAAO,MAAM,OAAO;;;;EAIkC,CAAA;AACtD,MAAM,MAAM,OAAO,GAAG,OAAO,OAAO,CAAC,IAAI,CAAA;AAEzC;;;GAGG;AACH,MAAM,WAAW,GAAG;IAClB,QAAQ,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,GAAG,CAAA;CAC1C;AACD,eAAO,MAAM,GAAG,6HAGiC,CAAA;AAEjD;;;GAGG;AACH,eAAO,MAAM,IAAI;;;;;;;;;;;;;;;;;;;;;;;EAckC,CAAA;AACnD,MAAM,MAAM,IAAI,GAAG,OAAO,IAAI,CAAC,IAAI,CAAA;AAEnC;;;GAGG;AACH,eAAO,MAAM,QAAQ;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAKkC,CAAA;AACvD,MAAM,MAAM,QAAQ,GAAG,OAAO,QAAQ,CAAC,IAAI,CAAA"}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { Schema } from 'effect';
|
|
2
|
+
/**
|
|
3
|
+
* The kind of a registry item or file. Mirrors shadcn's registry item types,
|
|
4
|
+
* trimmed to what FoldKit projects need.
|
|
5
|
+
*/
|
|
6
|
+
export const ItemType = Schema.Literals([
|
|
7
|
+
'registry:ui',
|
|
8
|
+
'registry:lib',
|
|
9
|
+
'registry:block',
|
|
10
|
+
'registry:theme',
|
|
11
|
+
'registry:file',
|
|
12
|
+
'registry:example',
|
|
13
|
+
]).annotate({ identifier: 'Foldcn.Registry.ItemType' });
|
|
14
|
+
/**
|
|
15
|
+
* A single distributable file within a registry item. `content` is inlined by
|
|
16
|
+
* `foldcn build`; `target` is required for `registry:file` items and resolved
|
|
17
|
+
* against the consumer project root.
|
|
18
|
+
*/
|
|
19
|
+
export const ItemFile = Schema.Struct({
|
|
20
|
+
path: Schema.String,
|
|
21
|
+
type: ItemType,
|
|
22
|
+
target: Schema.optionalKey(Schema.String),
|
|
23
|
+
content: Schema.optionalKey(Schema.String),
|
|
24
|
+
}).annotate({ identifier: 'Foldcn.Registry.ItemFile' });
|
|
25
|
+
/**
|
|
26
|
+
* CSS variables contributed by an item, merged into the consumer stylesheet.
|
|
27
|
+
* `theme` lands in `@theme inline`, `light` in `:root`, `dark` in `.dark`.
|
|
28
|
+
*/
|
|
29
|
+
export const CssVars = Schema.Struct({
|
|
30
|
+
theme: Schema.optionalKey(Schema.Record(Schema.String, Schema.String)),
|
|
31
|
+
light: Schema.optionalKey(Schema.Record(Schema.String, Schema.String)),
|
|
32
|
+
dark: Schema.optionalKey(Schema.Record(Schema.String, Schema.String)),
|
|
33
|
+
}).annotate({ identifier: 'Foldcn.Registry.CssVars' });
|
|
34
|
+
export const Css = Schema.Record(Schema.String, Schema.Union([Schema.String, Schema.suspend(() => Css)])).annotate({ identifier: 'Foldcn.Registry.Css' });
|
|
35
|
+
/**
|
|
36
|
+
* A registry item: one distributable unit (component, lib file, theme, block,
|
|
37
|
+
* or arbitrary file) with its npm and registry dependencies.
|
|
38
|
+
*/
|
|
39
|
+
export const Item = Schema.Struct({
|
|
40
|
+
$schema: Schema.optionalKey(Schema.String),
|
|
41
|
+
name: Schema.String,
|
|
42
|
+
type: ItemType,
|
|
43
|
+
title: Schema.optionalKey(Schema.String),
|
|
44
|
+
description: Schema.optionalKey(Schema.String),
|
|
45
|
+
dependencies: Schema.optionalKey(Schema.Array(Schema.String)),
|
|
46
|
+
devDependencies: Schema.optionalKey(Schema.Array(Schema.String)),
|
|
47
|
+
registryDependencies: Schema.optionalKey(Schema.Array(Schema.String)),
|
|
48
|
+
files: Schema.optionalKey(Schema.Array(ItemFile)),
|
|
49
|
+
cssVars: Schema.optionalKey(CssVars),
|
|
50
|
+
css: Schema.optionalKey(Css),
|
|
51
|
+
docs: Schema.optionalKey(Schema.String),
|
|
52
|
+
categories: Schema.optionalKey(Schema.Array(Schema.String)),
|
|
53
|
+
}).annotate({ identifier: 'Foldcn.Registry.Item' });
|
|
54
|
+
/**
|
|
55
|
+
* The registry manifest (`registry.json`): the list of items a registry
|
|
56
|
+
* publishes, built into per-item JSON by `foldcn build`.
|
|
57
|
+
*/
|
|
58
|
+
export const Manifest = Schema.Struct({
|
|
59
|
+
$schema: Schema.optionalKey(Schema.String),
|
|
60
|
+
name: Schema.String,
|
|
61
|
+
homepage: Schema.String,
|
|
62
|
+
items: Schema.Array(Item),
|
|
63
|
+
}).annotate({ identifier: 'Foldcn.Registry.Manifest' });
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/resolveTree/index.ts"],"names":[],"mappings":"AAAA,cAAc,eAAe,CAAA"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './resolveTree';
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { Effect, Option } from 'effect';
|
|
2
|
+
import type { Css, CssVars, Item, ItemFile } from '../registry';
|
|
3
|
+
/**
|
|
4
|
+
* A fetched registry item together with the canonical key it was resolved
|
|
5
|
+
* under. The key deduplicates items that different specifiers resolve to.
|
|
6
|
+
*/
|
|
7
|
+
export interface FetchedItem {
|
|
8
|
+
readonly key: string;
|
|
9
|
+
readonly item: Item;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* The merged closure of a set of registry items: everything `foldcn add`
|
|
13
|
+
* needs to write files, install packages, and update the stylesheet.
|
|
14
|
+
*/
|
|
15
|
+
export interface ResolvedTree {
|
|
16
|
+
readonly items: ReadonlyArray<Item>;
|
|
17
|
+
readonly dependencies: ReadonlyArray<string>;
|
|
18
|
+
readonly devDependencies: ReadonlyArray<string>;
|
|
19
|
+
readonly files: ReadonlyArray<ItemFile>;
|
|
20
|
+
readonly cssVars: CssVars;
|
|
21
|
+
readonly css: Css;
|
|
22
|
+
readonly warnings: ReadonlyArray<string>;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Resolves the `registryDependencies` closure of the given specifiers via
|
|
26
|
+
* breadth-first traversal. `getItem` performs the actual fetch and returns a
|
|
27
|
+
* canonical key per item; cycles and diamonds are deduplicated on that key.
|
|
28
|
+
* `maybeOrigin` carries the key of the item that requested the dependency so
|
|
29
|
+
* bare names can resolve against their origin registry.
|
|
30
|
+
*/
|
|
31
|
+
export declare const resolve: <E, R>(rootSpecs: ReadonlyArray<string>, getItem: (spec: string, maybeOrigin: Option.Option<string>) => Effect.Effect<FetchedItem, E, R>) => Effect.Effect<ResolvedTree, E, R>;
|
|
32
|
+
/**
|
|
33
|
+
* Merges an ordered list of items into a single plan. Npm dependencies are
|
|
34
|
+
* deduplicated by package name (first version-qualified specifier wins),
|
|
35
|
+
* files by target (first wins), and cssVars/css deep-merge first-wins per
|
|
36
|
+
* key. Conflicts produce warnings instead of failures.
|
|
37
|
+
*/
|
|
38
|
+
export declare const mergeItems: (items: ReadonlyArray<Item>) => ResolvedTree;
|
|
39
|
+
//# sourceMappingURL=resolveTree.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resolveTree.d.ts","sourceRoot":"","sources":["../../src/resolveTree/resolveTree.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAA;AAEvC,OAAO,KAAK,EAAE,GAAG,EAAE,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAA;AAE/D;;;GAGG;AACH,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAA;IACpB,QAAQ,CAAC,IAAI,EAAE,IAAI,CAAA;CACpB;AAED;;;GAGG;AACH,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,KAAK,EAAE,aAAa,CAAC,IAAI,CAAC,CAAA;IACnC,QAAQ,CAAC,YAAY,EAAE,aAAa,CAAC,MAAM,CAAC,CAAA;IAC5C,QAAQ,CAAC,eAAe,EAAE,aAAa,CAAC,MAAM,CAAC,CAAA;IAC/C,QAAQ,CAAC,KAAK,EAAE,aAAa,CAAC,QAAQ,CAAC,CAAA;IACvC,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAA;IACzB,QAAQ,CAAC,GAAG,EAAE,GAAG,CAAA;IACjB,QAAQ,CAAC,QAAQ,EAAE,aAAa,CAAC,MAAM,CAAC,CAAA;CACzC;AAID;;;;;;GAMG;AACH,eAAO,MAAM,OAAO,GAAI,CAAC,EAAE,CAAC,EAC1B,WAAW,aAAa,CAAC,MAAM,CAAC,EAChC,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,KAAK,MAAM,CAAC,MAAM,CAAC,WAAW,EAAE,CAAC,EAAE,CAAC,CAAC,KAC9F,MAAM,CAAC,MAAM,CAAC,YAAY,EAAE,CAAC,EAAE,CAAC,CAqC/B,CAAA;AAEJ;;;;;GAKG;AACH,eAAO,MAAM,UAAU,GAAI,OAAO,aAAa,CAAC,IAAI,CAAC,KAAG,YAyCvD,CAAA"}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { Effect, Option } from 'effect';
|
|
2
|
+
const fetchConcurrency = 8;
|
|
3
|
+
/**
|
|
4
|
+
* Resolves the `registryDependencies` closure of the given specifiers via
|
|
5
|
+
* breadth-first traversal. `getItem` performs the actual fetch and returns a
|
|
6
|
+
* canonical key per item; cycles and diamonds are deduplicated on that key.
|
|
7
|
+
* `maybeOrigin` carries the key of the item that requested the dependency so
|
|
8
|
+
* bare names can resolve against their origin registry.
|
|
9
|
+
*/
|
|
10
|
+
export const resolve = (rootSpecs, getItem) => Effect.gen(function* () {
|
|
11
|
+
const seenKeys = new Set();
|
|
12
|
+
const seenRequests = new Set();
|
|
13
|
+
const ordered = [];
|
|
14
|
+
let frontier = rootSpecs.map((spec) => [spec, Option.none()]);
|
|
15
|
+
while (frontier.length > 0) {
|
|
16
|
+
const requests = frontier.filter(([spec, maybeOrigin]) => {
|
|
17
|
+
const requestKey = `${Option.getOrElse(maybeOrigin, () => '')}::${spec}`;
|
|
18
|
+
if (seenRequests.has(requestKey)) {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
seenRequests.add(requestKey);
|
|
22
|
+
return true;
|
|
23
|
+
});
|
|
24
|
+
const fetched = yield* Effect.forEach(requests, ([spec, maybeOrigin]) => getItem(spec, maybeOrigin), {
|
|
25
|
+
concurrency: fetchConcurrency,
|
|
26
|
+
});
|
|
27
|
+
const next = [];
|
|
28
|
+
for (const { key, item } of fetched) {
|
|
29
|
+
if (seenKeys.has(key)) {
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
seenKeys.add(key);
|
|
33
|
+
ordered.push(item);
|
|
34
|
+
for (const dependency of item.registryDependencies ?? []) {
|
|
35
|
+
next.push([dependency, Option.some(key)]);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
frontier = next;
|
|
39
|
+
}
|
|
40
|
+
return mergeItems(ordered);
|
|
41
|
+
});
|
|
42
|
+
/**
|
|
43
|
+
* Merges an ordered list of items into a single plan. Npm dependencies are
|
|
44
|
+
* deduplicated by package name (first version-qualified specifier wins),
|
|
45
|
+
* files by target (first wins), and cssVars/css deep-merge first-wins per
|
|
46
|
+
* key. Conflicts produce warnings instead of failures.
|
|
47
|
+
*/
|
|
48
|
+
export const mergeItems = (items) => {
|
|
49
|
+
const warnings = [];
|
|
50
|
+
const dependencies = mergeDependencyLists(items.map((item) => item.dependencies ?? []), warnings);
|
|
51
|
+
const devDependencies = mergeDependencyLists(items.map((item) => item.devDependencies ?? []), warnings);
|
|
52
|
+
const files = [];
|
|
53
|
+
const seenFiles = new Map();
|
|
54
|
+
for (const item of items) {
|
|
55
|
+
for (const file of item.files ?? []) {
|
|
56
|
+
const fileKey = file.target ?? file.path;
|
|
57
|
+
const existing = seenFiles.get(fileKey);
|
|
58
|
+
if (existing === undefined) {
|
|
59
|
+
seenFiles.set(fileKey, file);
|
|
60
|
+
files.push(file);
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
if (existing.content !== file.content) {
|
|
64
|
+
warnings.push(`Skipped duplicate file "${fileKey}" from item "${item.name}" with divergent content`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
let cssVars = {};
|
|
69
|
+
let css = {};
|
|
70
|
+
for (const item of items) {
|
|
71
|
+
if (item.cssVars !== undefined) {
|
|
72
|
+
cssVars = mergeCssVars(cssVars, item.cssVars, item.name, warnings);
|
|
73
|
+
}
|
|
74
|
+
if (item.css !== undefined) {
|
|
75
|
+
css = mergeCss(css, item.css, item.name, warnings);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return { items, dependencies, devDependencies, files, cssVars, css, warnings };
|
|
79
|
+
};
|
|
80
|
+
const packageName = (spec) => {
|
|
81
|
+
const versionSeparator = spec.lastIndexOf('@');
|
|
82
|
+
if (versionSeparator > 0) {
|
|
83
|
+
return spec.slice(0, versionSeparator);
|
|
84
|
+
}
|
|
85
|
+
return spec;
|
|
86
|
+
};
|
|
87
|
+
const mergeDependencyLists = (lists, warnings) => {
|
|
88
|
+
const byName = new Map();
|
|
89
|
+
for (const list of lists) {
|
|
90
|
+
for (const spec of list) {
|
|
91
|
+
const name = packageName(spec);
|
|
92
|
+
const existing = byName.get(name);
|
|
93
|
+
if (existing === undefined) {
|
|
94
|
+
byName.set(name, spec);
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
if (existing !== spec) {
|
|
98
|
+
warnings.push(`Kept dependency "${existing}" over conflicting "${spec}"`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return Array.from(byName.values());
|
|
103
|
+
};
|
|
104
|
+
const mergeVarRecord = (existing, incoming, scope, itemName, warnings) => {
|
|
105
|
+
if (incoming === undefined) {
|
|
106
|
+
return existing;
|
|
107
|
+
}
|
|
108
|
+
if (existing === undefined) {
|
|
109
|
+
return incoming;
|
|
110
|
+
}
|
|
111
|
+
const merged = { ...existing };
|
|
112
|
+
for (const [key, value] of Object.entries(incoming)) {
|
|
113
|
+
const current = merged[key];
|
|
114
|
+
if (current === undefined) {
|
|
115
|
+
merged[key] = value;
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
if (current !== value) {
|
|
119
|
+
warnings.push(`Kept ${scope} variable "${key}: ${current}" over "${value}" from item "${itemName}"`);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return merged;
|
|
123
|
+
};
|
|
124
|
+
const mergeCssVars = (existing, incoming, itemName, warnings) => {
|
|
125
|
+
const theme = mergeVarRecord(existing.theme, incoming.theme, 'theme', itemName, warnings);
|
|
126
|
+
const light = mergeVarRecord(existing.light, incoming.light, 'light', itemName, warnings);
|
|
127
|
+
const dark = mergeVarRecord(existing.dark, incoming.dark, 'dark', itemName, warnings);
|
|
128
|
+
return {
|
|
129
|
+
...(theme === undefined ? {} : { theme }),
|
|
130
|
+
...(light === undefined ? {} : { light }),
|
|
131
|
+
...(dark === undefined ? {} : { dark }),
|
|
132
|
+
};
|
|
133
|
+
};
|
|
134
|
+
const mergeCss = (existing, incoming, itemName, warnings) => {
|
|
135
|
+
const merged = { ...existing };
|
|
136
|
+
for (const [selector, value] of Object.entries(incoming)) {
|
|
137
|
+
const current = merged[selector];
|
|
138
|
+
if (current === undefined) {
|
|
139
|
+
merged[selector] = value;
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
if (typeof current === 'object' && typeof value === 'object') {
|
|
143
|
+
merged[selector] = mergeCss(current, value, itemName, warnings);
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
if (current !== value) {
|
|
147
|
+
warnings.push(`Kept css "${selector}" over conflicting definition from item "${itemName}"`);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return merged;
|
|
151
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json.schemastore.org/package.json",
|
|
3
|
+
"name": "@foldcn/registry",
|
|
4
|
+
"version": "0.0.1",
|
|
5
|
+
"description": "Registry schemas and pure logic for foldcn",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"default": "./dist/index.js"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"dist"
|
|
16
|
+
],
|
|
17
|
+
"publishConfig": {
|
|
18
|
+
"access": "public"
|
|
19
|
+
},
|
|
20
|
+
"scripts": {
|
|
21
|
+
"build": "tsc -b tsconfig.build.json",
|
|
22
|
+
"test": "vitest run --passWithNoTests",
|
|
23
|
+
"typecheck": "tsc --noEmit"
|
|
24
|
+
},
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"effect": "4.0.0-beta.88",
|
|
27
|
+
"postcss": "8.5.16"
|
|
28
|
+
},
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"@effect/vitest": "4.0.0-beta.88",
|
|
31
|
+
"typescript": "6.0.3",
|
|
32
|
+
"vitest": "4.1.9"
|
|
33
|
+
}
|
|
34
|
+
}
|