@withstudiocms/component-registry 0.1.0-beta.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/CHANGELOG.md +10 -0
- package/LICENSE +21 -0
- package/README.md +308 -0
- package/dist/component-proxy/decoder/decode-codepoint.d.ts +18 -0
- package/dist/component-proxy/decoder/decode-codepoint.js +58 -0
- package/dist/component-proxy/decoder/decode-data-html.d.ts +1 -0
- package/dist/component-proxy/decoder/decode-data-html.js +7 -0
- package/dist/component-proxy/decoder/decode-data-xml.d.ts +1 -0
- package/dist/component-proxy/decoder/decode-data-xml.js +7 -0
- package/dist/component-proxy/decoder/index.d.ts +34 -0
- package/dist/component-proxy/decoder/index.js +18 -0
- package/dist/component-proxy/decoder/util.d.ts +208 -0
- package/dist/component-proxy/decoder/util.js +415 -0
- package/dist/component-proxy/index.d.ts +14 -0
- package/dist/component-proxy/index.js +39 -0
- package/dist/errors.d.ts +76 -0
- package/dist/errors.js +48 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +6 -0
- package/dist/registry/PropsParser.d.ts +15 -0
- package/dist/registry/PropsParser.js +210 -0
- package/dist/registry/Registry.d.ts +44 -0
- package/dist/registry/Registry.js +77 -0
- package/dist/registry/consts.d.ts +42 -0
- package/dist/registry/consts.js +19 -0
- package/dist/registry/handler.d.ts +92 -0
- package/dist/registry/handler.js +161 -0
- package/dist/registry/index.d.ts +3 -0
- package/dist/registry/index.js +3 -0
- package/dist/runtime.d.ts +34 -0
- package/dist/runtime.js +49 -0
- package/dist/transform-html.d.ts +11 -0
- package/dist/transform-html.js +10 -0
- package/dist/types.d.ts +64 -0
- package/dist/types.js +0 -0
- package/dist/utils.d.ts +52 -0
- package/dist/utils.js +41 -0
- package/dist/virtual.d.ts +80 -0
- package/package.json +90 -0
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { AstroError } from 'astro/errors';
|
|
2
|
+
export declare class ComponentProxyError extends AstroError {
|
|
3
|
+
message: string;
|
|
4
|
+
stack?: string;
|
|
5
|
+
name: string;
|
|
6
|
+
constructor(message: string, hint: string, stack?: string);
|
|
7
|
+
}
|
|
8
|
+
export declare function prefixError(err: Error, prefix: string): Error;
|
|
9
|
+
declare const ComponentRegistryError_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").Equals<A, {}> extends true ? void : { readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }) => import("effect/Cause").YieldableError & {
|
|
10
|
+
readonly _tag: "ComponentRegistryError";
|
|
11
|
+
} & Readonly<A>;
|
|
12
|
+
/**
|
|
13
|
+
* Error class representing issues related to the component registry.
|
|
14
|
+
*
|
|
15
|
+
* @remarks
|
|
16
|
+
* This error extends a tagged error type with the tag `'ComponentRegistryError'`.
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* ```typescript
|
|
20
|
+
* throw new ComponentRegistryError({ message: "Component not found" });
|
|
21
|
+
* ```
|
|
22
|
+
*
|
|
23
|
+
* @property message - A descriptive error message.
|
|
24
|
+
* @property cause - (Optional) The underlying cause of the error, if any.
|
|
25
|
+
*/
|
|
26
|
+
export declare class ComponentRegistryError extends ComponentRegistryError_base<{
|
|
27
|
+
readonly message: string;
|
|
28
|
+
readonly cause?: unknown;
|
|
29
|
+
}> {
|
|
30
|
+
}
|
|
31
|
+
declare const FileParseError_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").Equals<A, {}> extends true ? void : { readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }) => import("effect/Cause").YieldableError & {
|
|
32
|
+
readonly _tag: "FileParseError";
|
|
33
|
+
} & Readonly<A>;
|
|
34
|
+
/**
|
|
35
|
+
* Error thrown when a file fails to parse.
|
|
36
|
+
*
|
|
37
|
+
* @remarks
|
|
38
|
+
* This error extends a tagged error type with the tag 'FileParseError'.
|
|
39
|
+
*
|
|
40
|
+
* @example
|
|
41
|
+
* ```typescript
|
|
42
|
+
* throw new FileParseError({ filePath: '/path/to/file', message: 'Invalid format' });
|
|
43
|
+
* ```
|
|
44
|
+
*
|
|
45
|
+
* @property filePath - The path of the file that failed to parse.
|
|
46
|
+
* @property message - A descriptive error message.
|
|
47
|
+
* @property cause - (Optional) The underlying cause of the error, if available.
|
|
48
|
+
*/
|
|
49
|
+
export declare class FileParseError extends FileParseError_base<{
|
|
50
|
+
readonly filePath: string;
|
|
51
|
+
readonly message: string;
|
|
52
|
+
readonly cause?: unknown;
|
|
53
|
+
}> {
|
|
54
|
+
}
|
|
55
|
+
declare const ComponentNotFoundError_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").Equals<A, {}> extends true ? void : { readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }) => import("effect/Cause").YieldableError & {
|
|
56
|
+
readonly _tag: "ComponentNotFoundError";
|
|
57
|
+
} & Readonly<A>;
|
|
58
|
+
/**
|
|
59
|
+
* Error thrown when a requested component cannot be found in the registry.
|
|
60
|
+
*
|
|
61
|
+
* @remarks
|
|
62
|
+
* This error extends a tagged error type with the tag 'ComponentNotFoundError'.
|
|
63
|
+
*
|
|
64
|
+
* @example
|
|
65
|
+
* ```typescript
|
|
66
|
+
* throw new ComponentNotFoundError({ componentName: 'MyComponent' });
|
|
67
|
+
* ```
|
|
68
|
+
*
|
|
69
|
+
* @property componentName - The name of the component that was not found.
|
|
70
|
+
*/
|
|
71
|
+
export declare class ComponentNotFoundError extends ComponentNotFoundError_base<{
|
|
72
|
+
readonly componentName: string;
|
|
73
|
+
}> {
|
|
74
|
+
}
|
|
75
|
+
export declare function toComponentProxyError(err: Error, prefix: string): ComponentProxyError;
|
|
76
|
+
export {};
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { Data } from "@withstudiocms/effect";
|
|
2
|
+
import { AstroError } from "astro/errors";
|
|
3
|
+
class ComponentProxyError extends AstroError {
|
|
4
|
+
message;
|
|
5
|
+
stack;
|
|
6
|
+
name = "Component Proxy Error";
|
|
7
|
+
constructor(message, hint, stack) {
|
|
8
|
+
super(message, hint);
|
|
9
|
+
this.message = message;
|
|
10
|
+
this.hint = hint;
|
|
11
|
+
this.stack = stack;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
function prefixError(err, prefix) {
|
|
15
|
+
if (err?.message) {
|
|
16
|
+
try {
|
|
17
|
+
err.message = `${prefix}:
|
|
18
|
+
${err.message}`;
|
|
19
|
+
return err;
|
|
20
|
+
} catch {
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
const wrappedError = new Error(`${prefix}${err ? `: ${err}` : ""}`);
|
|
24
|
+
try {
|
|
25
|
+
wrappedError.stack = err.stack;
|
|
26
|
+
wrappedError.cause = err;
|
|
27
|
+
} catch {
|
|
28
|
+
}
|
|
29
|
+
return wrappedError;
|
|
30
|
+
}
|
|
31
|
+
class ComponentRegistryError extends Data.TaggedError("ComponentRegistryError") {
|
|
32
|
+
}
|
|
33
|
+
class FileParseError extends Data.TaggedError("FileParseError") {
|
|
34
|
+
}
|
|
35
|
+
class ComponentNotFoundError extends Data.TaggedError("ComponentNotFoundError") {
|
|
36
|
+
}
|
|
37
|
+
function toComponentProxyError(err, prefix) {
|
|
38
|
+
const wrapped = prefixError(err, prefix);
|
|
39
|
+
return new ComponentProxyError(wrapped.message, wrapped.message, wrapped.stack);
|
|
40
|
+
}
|
|
41
|
+
export {
|
|
42
|
+
ComponentNotFoundError,
|
|
43
|
+
ComponentProxyError,
|
|
44
|
+
ComponentRegistryError,
|
|
45
|
+
FileParseError,
|
|
46
|
+
prefixError,
|
|
47
|
+
toComponentProxyError
|
|
48
|
+
};
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { Effect } from '@withstudiocms/effect';
|
|
2
|
+
import { ComponentRegistryError, FileParseError } from '../errors.js';
|
|
3
|
+
import type { AstroComponentProps } from '../types.js';
|
|
4
|
+
declare const PropsParser_base: Effect.Service.Class<PropsParser, "PropsParser", {
|
|
5
|
+
readonly effect: Effect.Effect<{
|
|
6
|
+
parseComponentProps: (sourceCode: string) => Effect.Effect<AstroComponentProps[], ComponentRegistryError, never>;
|
|
7
|
+
extractPropsFromAstroFile: (astroFileContent: string) => Effect.Effect<string, FileParseError, never>;
|
|
8
|
+
}, never, never>;
|
|
9
|
+
}>;
|
|
10
|
+
/**
|
|
11
|
+
* Service for parsing component props from TypeScript source code and extracting prop definitions from Astro files.
|
|
12
|
+
*/
|
|
13
|
+
export declare class PropsParser extends PropsParser_base {
|
|
14
|
+
}
|
|
15
|
+
export {};
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import { Effect } from "@withstudiocms/effect";
|
|
2
|
+
import { Project, SyntaxKind } from "ts-morph";
|
|
3
|
+
import { ComponentRegistryError, FileParseError } from "../errors.js";
|
|
4
|
+
class PropsParser extends Effect.Service()("PropsParser", {
|
|
5
|
+
effect: Effect.gen(function* () {
|
|
6
|
+
return {
|
|
7
|
+
parseComponentProps: (sourceCode) => Effect.try({
|
|
8
|
+
try: () => {
|
|
9
|
+
const project = new Project();
|
|
10
|
+
const sourceFile = project.createSourceFile("temp.ts", sourceCode);
|
|
11
|
+
const results = [];
|
|
12
|
+
const extractJSDocInfo = (node) => {
|
|
13
|
+
const jsDocComments = node.getJsDocs();
|
|
14
|
+
let description;
|
|
15
|
+
let defaultValue;
|
|
16
|
+
const jsDocTags = [];
|
|
17
|
+
if (jsDocComments.length > 0) {
|
|
18
|
+
description = jsDocComments[0].getDescription().trim();
|
|
19
|
+
for (const jsDoc of jsDocComments) {
|
|
20
|
+
const tags = jsDoc.getTags();
|
|
21
|
+
for (const tag of tags) {
|
|
22
|
+
const tagName = tag.getTagName();
|
|
23
|
+
const commentText = tag.getCommentText();
|
|
24
|
+
switch (tagName) {
|
|
25
|
+
case "param": {
|
|
26
|
+
const paramInfo = tag.getStructure();
|
|
27
|
+
jsDocTags.push({
|
|
28
|
+
tagName,
|
|
29
|
+
text: commentText,
|
|
30
|
+
name: paramInfo.tagName,
|
|
31
|
+
type: typeof paramInfo.text === "string" ? paramInfo.text : void 0
|
|
32
|
+
});
|
|
33
|
+
break;
|
|
34
|
+
}
|
|
35
|
+
case "default": {
|
|
36
|
+
defaultValue = commentText;
|
|
37
|
+
jsDocTags.push({
|
|
38
|
+
tagName,
|
|
39
|
+
text: commentText
|
|
40
|
+
});
|
|
41
|
+
break;
|
|
42
|
+
}
|
|
43
|
+
case "example":
|
|
44
|
+
case "since":
|
|
45
|
+
case "deprecated":
|
|
46
|
+
case "see":
|
|
47
|
+
case "author":
|
|
48
|
+
case "version":
|
|
49
|
+
case "throws":
|
|
50
|
+
case "returns":
|
|
51
|
+
case "readonly":
|
|
52
|
+
case "internal":
|
|
53
|
+
case "beta":
|
|
54
|
+
case "alpha":
|
|
55
|
+
case "experimental": {
|
|
56
|
+
jsDocTags.push({
|
|
57
|
+
tagName,
|
|
58
|
+
text: commentText
|
|
59
|
+
});
|
|
60
|
+
break;
|
|
61
|
+
}
|
|
62
|
+
default: {
|
|
63
|
+
jsDocTags.push({
|
|
64
|
+
tagName,
|
|
65
|
+
text: commentText
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return { description, defaultValue, jsDocTags };
|
|
73
|
+
};
|
|
74
|
+
const interfaces = sourceFile.getInterfaces();
|
|
75
|
+
for (const interfaceDecl of interfaces) {
|
|
76
|
+
const interfaceName = interfaceDecl.getName();
|
|
77
|
+
const props = [];
|
|
78
|
+
const properties = interfaceDecl.getProperties();
|
|
79
|
+
for (const property of properties) {
|
|
80
|
+
const propName = property.getName();
|
|
81
|
+
const propType = property.getTypeNode()?.getText() || "unknown";
|
|
82
|
+
const isOptional = property.hasQuestionToken();
|
|
83
|
+
const { description, defaultValue, jsDocTags } = extractJSDocInfo(property);
|
|
84
|
+
props.push({
|
|
85
|
+
name: propName,
|
|
86
|
+
type: propType,
|
|
87
|
+
optional: isOptional,
|
|
88
|
+
description,
|
|
89
|
+
defaultValue,
|
|
90
|
+
jsDocTags: jsDocTags.length > 0 ? jsDocTags : void 0
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
results.push({ name: interfaceName, props });
|
|
94
|
+
}
|
|
95
|
+
const typeAliases = sourceFile.getTypeAliases();
|
|
96
|
+
for (const typeAlias of typeAliases) {
|
|
97
|
+
const typeName = typeAlias.getName();
|
|
98
|
+
const typeNode = typeAlias.getTypeNode();
|
|
99
|
+
if (typeNode && typeNode.getKind() === SyntaxKind.TypeLiteral) {
|
|
100
|
+
const props = [];
|
|
101
|
+
const typeLiteral = typeNode.asKindOrThrow(SyntaxKind.TypeLiteral);
|
|
102
|
+
const members = typeLiteral.getMembers();
|
|
103
|
+
for (const member of members) {
|
|
104
|
+
if (member.getKind() === SyntaxKind.PropertySignature) {
|
|
105
|
+
const propSig = member.asKindOrThrow(SyntaxKind.PropertySignature);
|
|
106
|
+
const propName = propSig.getName();
|
|
107
|
+
const propType = propSig.getTypeNode()?.getText() || "unknown";
|
|
108
|
+
const isOptional = propSig.hasQuestionToken();
|
|
109
|
+
const { description, defaultValue, jsDocTags } = extractJSDocInfo(propSig);
|
|
110
|
+
props.push({
|
|
111
|
+
name: propName,
|
|
112
|
+
type: propType,
|
|
113
|
+
optional: isOptional,
|
|
114
|
+
description,
|
|
115
|
+
defaultValue,
|
|
116
|
+
jsDocTags: jsDocTags.length > 0 ? jsDocTags : void 0
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
results.push({ name: typeName, props });
|
|
121
|
+
} else {
|
|
122
|
+
console.log(
|
|
123
|
+
`Type alias ${typeName} is not a type literal, kind: ${typeNode?.getKindName()}`
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return results;
|
|
128
|
+
},
|
|
129
|
+
catch: (error) => {
|
|
130
|
+
console.error("Error parsing component props:", error);
|
|
131
|
+
return new ComponentRegistryError({
|
|
132
|
+
message: "Failed to parse component props",
|
|
133
|
+
cause: error
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
}),
|
|
137
|
+
extractPropsFromAstroFile: (astroFileContent) => Effect.try({
|
|
138
|
+
try: () => {
|
|
139
|
+
const frontmatterMatch = astroFileContent.match(/^---\s*\n([\s\S]*?)\n---/m);
|
|
140
|
+
if (!frontmatterMatch) {
|
|
141
|
+
throw new Error("No frontmatter found in Astro file");
|
|
142
|
+
}
|
|
143
|
+
const frontmatter = frontmatterMatch[1];
|
|
144
|
+
const interfaceMatch = frontmatter.match(
|
|
145
|
+
/((?:export\s+)?interface\s+Props\s*\{[\s\S]*?\n\})/m
|
|
146
|
+
);
|
|
147
|
+
if (interfaceMatch) {
|
|
148
|
+
const propsDefinition2 = interfaceMatch[0].replace(/^export\s+/, "");
|
|
149
|
+
return propsDefinition2;
|
|
150
|
+
}
|
|
151
|
+
const typeMatch = frontmatter.match(
|
|
152
|
+
/((?:export\s+)?type\s+Props\s*=\s*\{[\s\S]*?\n\})/m
|
|
153
|
+
);
|
|
154
|
+
if (typeMatch) {
|
|
155
|
+
const propsDefinition2 = typeMatch[0].replace(/^export\s+/, "");
|
|
156
|
+
return propsDefinition2;
|
|
157
|
+
}
|
|
158
|
+
const propsStart = frontmatter.search(
|
|
159
|
+
/(?:export\s+)?(?:interface|type)\s+Props\s*[={]/
|
|
160
|
+
);
|
|
161
|
+
if (propsStart === -1) {
|
|
162
|
+
throw new Error("No Props interface or type found in frontmatter");
|
|
163
|
+
}
|
|
164
|
+
const propsSubstring = frontmatter.substring(propsStart);
|
|
165
|
+
let braceCount = 0;
|
|
166
|
+
let inString = false;
|
|
167
|
+
let stringChar = "";
|
|
168
|
+
let i = 0;
|
|
169
|
+
for (i = 0; i < propsSubstring.length; i++) {
|
|
170
|
+
const char = propsSubstring[i];
|
|
171
|
+
const prevChar = i > 0 ? propsSubstring[i - 1] : "";
|
|
172
|
+
if ((char === '"' || char === "'" || char === "`") && prevChar !== "\\") {
|
|
173
|
+
if (!inString) {
|
|
174
|
+
inString = true;
|
|
175
|
+
stringChar = char;
|
|
176
|
+
} else if (char === stringChar) {
|
|
177
|
+
inString = false;
|
|
178
|
+
stringChar = "";
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
if (!inString) {
|
|
182
|
+
if (char === "{") {
|
|
183
|
+
braceCount++;
|
|
184
|
+
} else if (char === "}") {
|
|
185
|
+
braceCount--;
|
|
186
|
+
if (braceCount === 0) {
|
|
187
|
+
break;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
const propsDefinition = propsSubstring.substring(0, i + 1).replace(/^export\s+/, "");
|
|
193
|
+
return propsDefinition;
|
|
194
|
+
},
|
|
195
|
+
catch: (error) => {
|
|
196
|
+
console.error("Error extracting props from Astro file:", error);
|
|
197
|
+
return new FileParseError({
|
|
198
|
+
filePath: "astro-content",
|
|
199
|
+
message: error instanceof Error ? error.message : "Failed to extract props from Astro file",
|
|
200
|
+
cause: error
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
})
|
|
204
|
+
};
|
|
205
|
+
})
|
|
206
|
+
}) {
|
|
207
|
+
}
|
|
208
|
+
export {
|
|
209
|
+
PropsParser
|
|
210
|
+
};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { Effect, Layer, Platform } from '@withstudiocms/effect';
|
|
2
|
+
import { ComponentNotFoundError, ComponentRegistryError, FileParseError } from '../errors.js';
|
|
3
|
+
import type { AstroComponentProps } from '../types.js';
|
|
4
|
+
import { PropsParser } from './PropsParser.js';
|
|
5
|
+
declare const ComponentRegistry_base: Effect.Service.Class<ComponentRegistry, "ComponentRegistry", {
|
|
6
|
+
readonly dependencies: readonly [Layer.Layer<PropsParser, never, never>, Layer.Layer<Platform.Path.Path, never, never>, Layer.Layer<Platform.FileSystem.FileSystem, never, never>];
|
|
7
|
+
readonly effect: Effect.Effect<{
|
|
8
|
+
registerComponentFromFile: (filePath: string, componentName?: string) => Effect.Effect<void, ComponentRegistryError | FileParseError | Platform.Error.PlatformError, never>;
|
|
9
|
+
getAllComponents: () => Effect.Effect<Map<string, AstroComponentProps>, never, never>;
|
|
10
|
+
getComponentProps: (componentName: string) => Effect.Effect<AstroComponentProps, ComponentNotFoundError, never>;
|
|
11
|
+
validateProps: (componentName: string, props: Record<string, unknown>) => Effect.Effect<{
|
|
12
|
+
valid: boolean;
|
|
13
|
+
errors: string[];
|
|
14
|
+
}, ComponentNotFoundError, never>;
|
|
15
|
+
}, never, never>;
|
|
16
|
+
}>;
|
|
17
|
+
/**
|
|
18
|
+
* A service class for registering, retrieving, and validating Astro component props.
|
|
19
|
+
*
|
|
20
|
+
* The `ComponentRegistry` provides methods to:
|
|
21
|
+
* - Register a component and its props from an Astro file.
|
|
22
|
+
* - Retrieve the props definition for a registered component.
|
|
23
|
+
* - List all registered components and their props.
|
|
24
|
+
* - Validate a set of props against a registered component's prop definition.
|
|
25
|
+
*
|
|
26
|
+
* Dependencies:
|
|
27
|
+
* - `PropsParser.Default`: Used to extract and parse props from Astro files.
|
|
28
|
+
* - `Path.layer`: Used for file path operations.
|
|
29
|
+
* - `NodeFileSystem.layer`: Used for reading files from the filesystem.
|
|
30
|
+
*
|
|
31
|
+
* Methods:
|
|
32
|
+
* - `registerComponentFromFile(filePath: string, componentName?: string)`: Registers a component by reading and parsing its props from the specified Astro file.
|
|
33
|
+
* - `getComponentProps(componentName: string)`: Retrieves the props definition for the specified component.
|
|
34
|
+
* - `getAllComponents()`: Returns a map of all registered components and their props.
|
|
35
|
+
* - `validateProps(componentName: string, props: Record<string, unknown>)`: Validates the provided props against the registered component's prop definition, checking for missing required props and unknown props.
|
|
36
|
+
*
|
|
37
|
+
* Errors:
|
|
38
|
+
* - Throws `FileParseError` if the Astro file cannot be parsed.
|
|
39
|
+
* - Throws `ComponentRegistryError` if registration fails.
|
|
40
|
+
* - Throws `ComponentNotFoundError` if a requested component is not registered.
|
|
41
|
+
*/
|
|
42
|
+
export declare class ComponentRegistry extends ComponentRegistry_base {
|
|
43
|
+
}
|
|
44
|
+
export {};
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { Effect, Layer, Platform, PlatformNode } from "@withstudiocms/effect";
|
|
2
|
+
import { ComponentNotFoundError, ComponentRegistryError, FileParseError } from "../errors.js";
|
|
3
|
+
import { PropsParser } from "./PropsParser.js";
|
|
4
|
+
const registryDeps = Layer.mergeAll(
|
|
5
|
+
PropsParser.Default,
|
|
6
|
+
Platform.Path.layer,
|
|
7
|
+
PlatformNode.NodeFileSystem.layer
|
|
8
|
+
);
|
|
9
|
+
class ComponentRegistry extends Effect.Service()("ComponentRegistry", {
|
|
10
|
+
dependencies: [PropsParser.Default, Platform.Path.layer, PlatformNode.NodeFileSystem.layer],
|
|
11
|
+
effect: Effect.gen(function* () {
|
|
12
|
+
const parser = yield* PropsParser;
|
|
13
|
+
const fs = yield* Platform.FileSystem.FileSystem;
|
|
14
|
+
const path = yield* Platform.Path.Path;
|
|
15
|
+
const components = /* @__PURE__ */ new Map();
|
|
16
|
+
return {
|
|
17
|
+
registerComponentFromFile: (filePath, componentName) => Effect.gen(function* () {
|
|
18
|
+
const fileContent = yield* fs.readFileString(filePath);
|
|
19
|
+
const propsDefinition = yield* parser.extractPropsFromAstroFile(fileContent).pipe(
|
|
20
|
+
Effect.mapError(
|
|
21
|
+
(error) => new FileParseError({
|
|
22
|
+
filePath,
|
|
23
|
+
message: error.message,
|
|
24
|
+
cause: error.cause
|
|
25
|
+
})
|
|
26
|
+
)
|
|
27
|
+
);
|
|
28
|
+
const name = componentName || path.basename(filePath, path.extname(filePath));
|
|
29
|
+
const parsed = yield* parser.parseComponentProps(propsDefinition).pipe(
|
|
30
|
+
Effect.mapError(
|
|
31
|
+
(error) => new ComponentRegistryError({
|
|
32
|
+
message: `Failed to register component ${name}`,
|
|
33
|
+
cause: error
|
|
34
|
+
})
|
|
35
|
+
)
|
|
36
|
+
);
|
|
37
|
+
if (parsed.length > 0) {
|
|
38
|
+
components.set(name, parsed[0]);
|
|
39
|
+
}
|
|
40
|
+
}),
|
|
41
|
+
getAllComponents: () => Effect.succeed(new Map(components)),
|
|
42
|
+
getComponentProps: (componentName) => Effect.gen(function* () {
|
|
43
|
+
const component = components.get(componentName);
|
|
44
|
+
if (!component) {
|
|
45
|
+
return yield* Effect.fail(new ComponentNotFoundError({ componentName }));
|
|
46
|
+
}
|
|
47
|
+
return component;
|
|
48
|
+
}),
|
|
49
|
+
validateProps: (componentName, props) => Effect.gen(function* () {
|
|
50
|
+
const component = components.get(componentName);
|
|
51
|
+
if (!component) {
|
|
52
|
+
return yield* Effect.fail(new ComponentNotFoundError({ componentName }));
|
|
53
|
+
}
|
|
54
|
+
const errors = [];
|
|
55
|
+
const providedProps = new Set(Object.keys(props));
|
|
56
|
+
if (component) {
|
|
57
|
+
for (const prop of component.props) {
|
|
58
|
+
if (!prop.optional && !providedProps.has(prop.name)) {
|
|
59
|
+
errors.push(`Required prop "${prop.name}" is missing`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
const validPropNames = new Set(component.props.map((p) => p.name));
|
|
63
|
+
for (const propName of providedProps) {
|
|
64
|
+
if (!validPropNames.has(propName)) {
|
|
65
|
+
errors.push(`Unknown prop "${propName}"`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return { valid: errors.length === 0, errors };
|
|
70
|
+
})
|
|
71
|
+
};
|
|
72
|
+
}).pipe(Effect.provide(registryDeps))
|
|
73
|
+
}) {
|
|
74
|
+
}
|
|
75
|
+
export {
|
|
76
|
+
ComponentRegistry
|
|
77
|
+
};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { ComponentRegistryEntry } from '../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Internal virtual module ID for the component registry.
|
|
4
|
+
*/
|
|
5
|
+
export declare const InternalId = "virtual:component-registry-internal-proxy";
|
|
6
|
+
/**
|
|
7
|
+
* Runtime virtual module ID for the component registry.
|
|
8
|
+
*/
|
|
9
|
+
export declare const RuntimeInternalId = "virtual:component-registry-internal-proxy/runtime";
|
|
10
|
+
/**
|
|
11
|
+
* Internal virtual module ID for the component registry.
|
|
12
|
+
*/
|
|
13
|
+
export declare const NameInternalId = "virtual:component-registry-internal-proxy/name";
|
|
14
|
+
/**
|
|
15
|
+
* Builds a virtual import string that exports component keys, component props, and component definitions.
|
|
16
|
+
*
|
|
17
|
+
* @param componentKeys - An array of strings representing the keys of the components to be registered.
|
|
18
|
+
* @param componentProps - An array of `ComponentRegistryEntry` objects containing the props for each component.
|
|
19
|
+
* @param components - An array of strings, each representing a component's code to be included in the export.
|
|
20
|
+
* @returns A string containing the virtual import code that exports the component keys, props, and component definitions.
|
|
21
|
+
*/
|
|
22
|
+
export declare const buildVirtualImport: (componentKeys: string[], componentProps: ComponentRegistryEntry[], components: string[]) => string;
|
|
23
|
+
/**
|
|
24
|
+
* Builds an object mapping virtual module IDs to their corresponding export statements.
|
|
25
|
+
*
|
|
26
|
+
* @param virtualId - The base virtual module ID to use as the key.
|
|
27
|
+
* @returns An object where:
|
|
28
|
+
* - The key `virtualId` maps to an export statement for `InternalId`.
|
|
29
|
+
* - The key `${virtualId}/runtime` maps to an export statement for `RuntimeInternalId`.
|
|
30
|
+
*
|
|
31
|
+
* @example
|
|
32
|
+
* ```typescript
|
|
33
|
+
* const exports = buildAliasExports('my-module');
|
|
34
|
+
* // {
|
|
35
|
+
* // 'my-module': "export * from 'InternalId';",
|
|
36
|
+
* // 'my-module/runtime': "export * from 'RuntimeInternalId';"
|
|
37
|
+
* // }
|
|
38
|
+
* ```
|
|
39
|
+
*/
|
|
40
|
+
export declare const buildAliasExports: (virtualId: string) => {
|
|
41
|
+
[x: string]: string;
|
|
42
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
const InternalId = "virtual:component-registry-internal-proxy";
|
|
2
|
+
const RuntimeInternalId = `${InternalId}/runtime`;
|
|
3
|
+
const NameInternalId = `${InternalId}/name`;
|
|
4
|
+
const buildVirtualImport = (componentKeys, componentProps, components) => `
|
|
5
|
+
export const componentKeys = ${JSON.stringify(componentKeys)};
|
|
6
|
+
export const componentProps = ${JSON.stringify(componentProps)};
|
|
7
|
+
${components ? components.join("\n") : ""}
|
|
8
|
+
`;
|
|
9
|
+
const buildAliasExports = (virtualId) => ({
|
|
10
|
+
[virtualId]: `export * from '${InternalId}';`,
|
|
11
|
+
[`${virtualId}/runtime`]: `export * from '${RuntimeInternalId}';`
|
|
12
|
+
});
|
|
13
|
+
export {
|
|
14
|
+
InternalId,
|
|
15
|
+
NameInternalId,
|
|
16
|
+
RuntimeInternalId,
|
|
17
|
+
buildAliasExports,
|
|
18
|
+
buildVirtualImport
|
|
19
|
+
};
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/// <reference types="../virtual.d.ts" preserve="true" />
|
|
2
|
+
import { z } from 'astro/zod';
|
|
3
|
+
export declare const ComponentRegistryHandlerOptionSchema: z.ZodObject<{
|
|
4
|
+
config: z.ZodObject<{
|
|
5
|
+
/**
|
|
6
|
+
* The name of the integration
|
|
7
|
+
*/
|
|
8
|
+
name: z.ZodString;
|
|
9
|
+
/**
|
|
10
|
+
* The virtual module ID for generated imports
|
|
11
|
+
*
|
|
12
|
+
* @example 'virtual:my-integration'
|
|
13
|
+
*/
|
|
14
|
+
virtualId: z.ZodOptional<z.ZodString>;
|
|
15
|
+
/**
|
|
16
|
+
* Enables verbose logging
|
|
17
|
+
*/
|
|
18
|
+
verbose: z.ZodOptional<z.ZodDefault<z.ZodBoolean>>;
|
|
19
|
+
}, "strip", z.ZodTypeAny, {
|
|
20
|
+
name: string;
|
|
21
|
+
verbose?: boolean | undefined;
|
|
22
|
+
virtualId?: string | undefined;
|
|
23
|
+
}, {
|
|
24
|
+
name: string;
|
|
25
|
+
verbose?: boolean | undefined;
|
|
26
|
+
virtualId?: string | undefined;
|
|
27
|
+
}>;
|
|
28
|
+
/**
|
|
29
|
+
* A record of user-provided component names to file paths.
|
|
30
|
+
*/
|
|
31
|
+
componentRegistry: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
32
|
+
/**
|
|
33
|
+
* A record of built-in component names to file paths.
|
|
34
|
+
*/
|
|
35
|
+
builtInComponents: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
36
|
+
}, "strip", z.ZodTypeAny, {
|
|
37
|
+
config: {
|
|
38
|
+
name: string;
|
|
39
|
+
verbose?: boolean | undefined;
|
|
40
|
+
virtualId?: string | undefined;
|
|
41
|
+
};
|
|
42
|
+
componentRegistry?: Record<string, string> | undefined;
|
|
43
|
+
builtInComponents?: Record<string, string> | undefined;
|
|
44
|
+
}, {
|
|
45
|
+
config: {
|
|
46
|
+
name: string;
|
|
47
|
+
verbose?: boolean | undefined;
|
|
48
|
+
virtualId?: string | undefined;
|
|
49
|
+
};
|
|
50
|
+
componentRegistry?: Record<string, string> | undefined;
|
|
51
|
+
builtInComponents?: Record<string, string> | undefined;
|
|
52
|
+
}>;
|
|
53
|
+
export type ComponentRegistryHandlerOptions = z.infer<typeof ComponentRegistryHandlerOptionSchema>;
|
|
54
|
+
/**
|
|
55
|
+
* Handles the setup and registration of components in the component registry during the Astro config setup phase.
|
|
56
|
+
*
|
|
57
|
+
* This utility merges built-in and user-provided component registries, validates and resolves component paths,
|
|
58
|
+
* registers components, and generates virtual imports for use at runtime. It also logs detailed information
|
|
59
|
+
* about the registration process, including the number of components processed and their properties.
|
|
60
|
+
*
|
|
61
|
+
* @remarks
|
|
62
|
+
* - Only components with string values ending in `.astro` are considered valid.
|
|
63
|
+
* - Uses Effect for error handling and asynchronous operations.
|
|
64
|
+
* - Generates virtual modules for internal proxying and runtime usage.
|
|
65
|
+
*
|
|
66
|
+
* @param params - The parameters provided by the Astro utility hook, including logger and config.
|
|
67
|
+
* @param options - An object containing configuration and registry options:
|
|
68
|
+
* - `config.verbose` - Enables verbose logging if true.
|
|
69
|
+
* - `config.name` - The name of the integration for logging purposes.
|
|
70
|
+
* - `config.virtualId` - The virtual module ID for generated imports.
|
|
71
|
+
* - `builtInComponents` - A record of built-in component names to file paths.
|
|
72
|
+
* - `componentRegistry` - A record of user-provided component names to file paths.
|
|
73
|
+
* @returns A promise that resolves after the component registry has been set up and virtual imports have been added.
|
|
74
|
+
*
|
|
75
|
+
* @example
|
|
76
|
+
* ```typescript
|
|
77
|
+
* await componentRegistryHandler(params, {
|
|
78
|
+
* config: { verbose: true, name: 'my-integration', virtualId: 'virtual:my-registry' },
|
|
79
|
+
* builtInComponents: { button: '/components/Button.astro' },
|
|
80
|
+
* componentRegistry: { card: '/components/Card.astro' }
|
|
81
|
+
* });
|
|
82
|
+
* ```
|
|
83
|
+
*/
|
|
84
|
+
export declare const componentRegistryHandler: import("astro-integration-kit").HookUtility<"astro:config:setup", [{
|
|
85
|
+
config: {
|
|
86
|
+
name: string;
|
|
87
|
+
verbose?: boolean | undefined;
|
|
88
|
+
virtualId?: string | undefined;
|
|
89
|
+
};
|
|
90
|
+
componentRegistry?: Record<string, string> | undefined;
|
|
91
|
+
builtInComponents?: Record<string, string> | undefined;
|
|
92
|
+
}], Promise<void>>;
|