@cyclone-ui/registry-api 0.0.0-stage → 0.3.33

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.
Files changed (81) hide show
  1. package/CHANGELOG.md +52 -0
  2. package/LICENSE +187 -0
  3. package/README.md +23 -2
  4. package/dist/client-B1f6qn7V.d.mts +30 -0
  5. package/dist/client-B1f6qn7V.d.mts.map +1 -0
  6. package/dist/client-CdEiR_5f.d.cts +30 -0
  7. package/dist/client-CdEiR_5f.d.cts.map +1 -0
  8. package/dist/client.cjs +1 -0
  9. package/dist/client.d.cts +2 -0
  10. package/dist/client.d.mts +2 -0
  11. package/dist/client.mjs +2 -0
  12. package/dist/client.mjs.map +1 -0
  13. package/dist/context-80NMoklc.d.cts +10 -0
  14. package/dist/context-80NMoklc.d.cts.map +1 -0
  15. package/dist/context-XjFUvK1j.d.mts +10 -0
  16. package/dist/context-XjFUvK1j.d.mts.map +1 -0
  17. package/dist/context.cjs +1 -0
  18. package/dist/context.d.cts +2 -0
  19. package/dist/context.d.mts +2 -0
  20. package/dist/context.mjs +2 -0
  21. package/dist/context.mjs.map +1 -0
  22. package/dist/errors-BntyHNM7.d.cts +7 -0
  23. package/dist/errors-BntyHNM7.d.cts.map +1 -0
  24. package/dist/errors-BntyHNM7.d.mts +7 -0
  25. package/dist/errors-BntyHNM7.d.mts.map +1 -0
  26. package/dist/errors.cjs +1 -0
  27. package/dist/errors.d.cts +2 -0
  28. package/dist/errors.d.mts +2 -0
  29. package/dist/errors.mjs +2 -0
  30. package/dist/errors.mjs.map +1 -0
  31. package/dist/index.cjs +1 -0
  32. package/dist/index.d.cts +6 -0
  33. package/dist/index.d.mts +6 -0
  34. package/dist/index.mjs +1 -0
  35. package/dist/router-B1KmNsNV.d.cts +29 -0
  36. package/dist/router-B1KmNsNV.d.cts.map +1 -0
  37. package/dist/router-CzaRhoPr.d.mts +29 -0
  38. package/dist/router-CzaRhoPr.d.mts.map +1 -0
  39. package/dist/router.cjs +1 -0
  40. package/dist/router.d.cts +2 -0
  41. package/dist/router.d.mts +2 -0
  42. package/dist/router.mjs +2 -0
  43. package/dist/router.mjs.map +1 -0
  44. package/dist/routes/components.cjs +1 -0
  45. package/dist/routes/components.d.cts +22 -0
  46. package/dist/routes/components.d.cts.map +1 -0
  47. package/dist/routes/components.d.mts +22 -0
  48. package/dist/routes/components.d.mts.map +1 -0
  49. package/dist/routes/components.mjs +2 -0
  50. package/dist/routes/components.mjs.map +1 -0
  51. package/dist/trpc.cjs +1 -0
  52. package/dist/trpc.d.cts +8 -0
  53. package/dist/trpc.d.cts.map +1 -0
  54. package/dist/trpc.d.mts +8 -0
  55. package/dist/trpc.d.mts.map +1 -0
  56. package/dist/trpc.mjs +2 -0
  57. package/dist/trpc.mjs.map +1 -0
  58. package/dist/types-Cn4R4SGM.d.cts +47 -0
  59. package/dist/types-Cn4R4SGM.d.cts.map +1 -0
  60. package/dist/types-Cn4R4SGM.d.mts +47 -0
  61. package/dist/types-Cn4R4SGM.d.mts.map +1 -0
  62. package/dist/types.cjs +1 -0
  63. package/dist/types.d.cts +2 -0
  64. package/dist/types.d.mts +2 -0
  65. package/dist/types.mjs +2 -0
  66. package/dist/types.mjs.map +1 -0
  67. package/package.json +147 -3
  68. package/powerlines.config.ts +28 -0
  69. package/project.json +8 -0
  70. package/src/client.ts +34 -0
  71. package/src/context.ts +41 -0
  72. package/src/errors.ts +25 -0
  73. package/src/index.ts +32 -0
  74. package/src/router.ts +27 -0
  75. package/src/routes/components.ts +175 -0
  76. package/src/trpc.ts +31 -0
  77. package/src/types.ts +70 -0
  78. package/test/routes/components.spec.ts +96 -0
  79. package/tsconfig.json +13 -0
  80. package/tsconfig.spec.json +16 -0
  81. package/vitest.config.mts +24 -0
@@ -0,0 +1,175 @@
1
+ /* -------------------------------------------------------------------
2
+
3
+ 🗲 Storm Software - Cyclone UI
4
+
5
+ This code was released as part of the Cyclone UI project. Cyclone UI
6
+ is maintained by Storm Software under the Apache-2.0 license, and is
7
+ free for commercial and private use. For more information, please visit
8
+ our licensing page at https://stormsoftware.com/licenses/projects/cyclone-ui.
9
+
10
+ Website: https://stormsoftware.com
11
+ Repository: https://github.com/storm-software/cyclone-ui
12
+ Documentation: https://docs.stormsoftware.com/projects/cyclone-ui
13
+ Contact: https://stormsoftware.com/contact
14
+
15
+ SPDX-License-Identifier: Apache-2.0
16
+
17
+ ------------------------------------------------------------------- */
18
+
19
+ import type { R2Object, R2ObjectBody } from "@cloudflare/workers-types";
20
+ import { z } from "zod";
21
+ import { createRouter, publicProcedure } from "../trpc";
22
+ import type {
23
+ ComponentDetails,
24
+ ComponentMeta,
25
+ ComponentSummary,
26
+ RegistryStorage
27
+ } from "../types";
28
+
29
+ interface RegistryStorageFile {
30
+ name: string;
31
+ content: R2ObjectBody | null;
32
+ updatedOn: Date;
33
+ }
34
+
35
+ const COMPONENTS_PREFIX = "registry/components/";
36
+
37
+ interface ComponentVersion {
38
+ componentName: string;
39
+ prefix: string;
40
+ metaObject: R2Object;
41
+ }
42
+
43
+ async function listAllObjects(
44
+ storage: RegistryStorage,
45
+ prefix: string
46
+ ): Promise<R2Object[]> {
47
+ const objects: R2Object[] = [];
48
+ let cursor: string | undefined;
49
+
50
+ do {
51
+ const result = await storage.list({ prefix, cursor });
52
+ objects.push(...result.objects);
53
+ cursor = result.truncated ? result.cursor : undefined;
54
+ } while (cursor);
55
+
56
+ return objects;
57
+ }
58
+
59
+ function getComponentVersion(storageObject: R2Object): ComponentVersion | null {
60
+ const relativeKey = storageObject.key.slice(COMPONENTS_PREFIX.length);
61
+ const segments = relativeKey.split("/").filter(Boolean);
62
+ if (segments.length < 2 || segments.at(-1) !== "meta.json") {
63
+ return null;
64
+ }
65
+
66
+ const componentName = segments[0]!;
67
+ const versionPath = segments.slice(1, -1).join("/");
68
+
69
+ return {
70
+ componentName,
71
+ prefix: `${COMPONENTS_PREFIX}${componentName}/${versionPath ? `${versionPath}/` : ""}`,
72
+ metaObject: storageObject
73
+ };
74
+ }
75
+
76
+ function getLatestVersions(objects: R2Object[]): ComponentVersion[] {
77
+ const versions = objects
78
+ .map(getComponentVersion)
79
+ .filter((value): value is ComponentVersion => Boolean(value));
80
+
81
+ return [...versions]
82
+ .sort(
83
+ (left, right) =>
84
+ right.metaObject.uploaded.getTime() - left.metaObject.uploaded.getTime()
85
+ )
86
+ .filter(
87
+ (version, index, all) =>
88
+ all.findIndex(
89
+ candidate => candidate.componentName === version.componentName
90
+ ) === index
91
+ );
92
+ }
93
+
94
+ export const componentsRouter = createRouter({
95
+ list: publicProcedure.query<ComponentSummary[]>(async ({ ctx }) => {
96
+ const latestVersions = getLatestVersions(
97
+ await listAllObjects(ctx.storage, COMPONENTS_PREFIX)
98
+ );
99
+
100
+ const components = await Promise.all(
101
+ latestVersions.map(async version => {
102
+ const content = await ctx.storage.get(version.metaObject.key);
103
+ if (!content) {
104
+ return null;
105
+ }
106
+
107
+ const meta = await content.json<ComponentMeta>();
108
+ return {
109
+ name: meta.name || version.componentName,
110
+ version: meta.version,
111
+ release: meta.release,
112
+ description: meta.description,
113
+ tags: meta.tags,
114
+ updatedOn: version.metaObject.uploaded
115
+ } as ComponentSummary;
116
+ })
117
+ );
118
+
119
+ return components.filter(Boolean) as ComponentSummary[];
120
+ }),
121
+ get: publicProcedure
122
+ .input(z.string())
123
+ .query<ComponentDetails>(async ({ input, ctx }) => {
124
+ const componentName = input.replace(/^@cyclone-ui\//, "");
125
+ const componentPrefix = `${COMPONENTS_PREFIX}${componentName}/`;
126
+ const storageObjects = await listAllObjects(ctx.storage, componentPrefix);
127
+ const latestVersion = getLatestVersions(storageObjects)[0];
128
+ if (!latestVersion) {
129
+ throw new Error(`Component \"${input}\" was not found`);
130
+ }
131
+
132
+ const storageFiles = (
133
+ await Promise.all(
134
+ storageObjects
135
+ .filter(storageObject =>
136
+ storageObject.key.startsWith(latestVersion.prefix)
137
+ )
138
+ .map(async (storageObject: R2Object) =>
139
+ ctx.storage
140
+ .get(storageObject.key)
141
+ .then((content: R2ObjectBody | null) => ({
142
+ name: storageObject.key.slice(latestVersion.prefix.length),
143
+ content,
144
+ updatedOn: storageObject.uploaded
145
+ }))
146
+ )
147
+ )
148
+ ).filter(Boolean) as RegistryStorageFile[];
149
+
150
+ const metaFile = storageFiles.find(
151
+ (file: RegistryStorageFile) => file.name === "meta.json"
152
+ );
153
+ if (!metaFile || !metaFile.content) {
154
+ throw new Error("Metadata file not found");
155
+ }
156
+
157
+ const meta = await metaFile.content.json<ComponentMeta>();
158
+ const componentFiles = storageFiles.filter(
159
+ file => file.name !== "meta.json" && file.content
160
+ );
161
+ const componentContent = await Promise.all(
162
+ componentFiles
163
+ .filter(file => file.name !== "meta.json" && file.content)
164
+ .map(async file => Promise.resolve(file.content!.text()))
165
+ );
166
+
167
+ return {
168
+ ...meta,
169
+ files: componentFiles.map((file, index) => ({
170
+ ...file,
171
+ content: componentContent[index]!
172
+ }))
173
+ };
174
+ })
175
+ });
package/src/trpc.ts ADDED
@@ -0,0 +1,31 @@
1
+ /* -------------------------------------------------------------------
2
+
3
+ 🗲 Storm Software - Cyclone UI
4
+
5
+ This code was released as part of the Cyclone UI project. Cyclone UI
6
+ is maintained by Storm Software under the Apache-2.0 license, and is
7
+ free for commercial and private use. For more information, please visit
8
+ our licensing page at https://stormsoftware.com/licenses/projects/cyclone-ui.
9
+
10
+ Website: https://stormsoftware.com
11
+ Repository: https://github.com/storm-software/cyclone-ui
12
+ Documentation: https://docs.stormsoftware.com/projects/cyclone-ui
13
+ Contact: https://stormsoftware.com/contact
14
+
15
+ SPDX-License-Identifier: Apache-2.0
16
+
17
+ ------------------------------------------------------------------- */
18
+
19
+ import type { TRPCRootObject } from "@trpc/server";
20
+ import { initTRPC } from "@trpc/server";
21
+ import type { Context } from "./context";
22
+
23
+ // eslint-disable-next-line ts/no-empty-object-type
24
+ export const t: TRPCRootObject<Context, object, {}> = initTRPC
25
+ .context<Context>()
26
+ .create();
27
+
28
+ export const publicProcedure: typeof t.procedure = t.procedure;
29
+ export const protectedProcedure: typeof t.procedure = t.procedure;
30
+
31
+ export const createRouter: typeof t.router = t.router;
package/src/types.ts ADDED
@@ -0,0 +1,70 @@
1
+ /* -------------------------------------------------------------------
2
+
3
+ 🗲 Storm Software - Cyclone UI
4
+
5
+ This code was released as part of the Cyclone UI project. Cyclone UI
6
+ is maintained by Storm Software under the Apache-2.0 license, and is
7
+ free for commercial and private use. For more information, please visit
8
+ our licensing page at https://stormsoftware.com/licenses/projects/cyclone-ui.
9
+
10
+ Website: https://stormsoftware.com
11
+ Repository: https://github.com/storm-software/cyclone-ui
12
+ Documentation: https://docs.stormsoftware.com/projects/cyclone-ui
13
+ Contact: https://stormsoftware.com/contact
14
+
15
+ SPDX-License-Identifier: Apache-2.0
16
+
17
+ ------------------------------------------------------------------- */
18
+
19
+ import type { R2ObjectBody, R2Objects } from "@cloudflare/workers-types";
20
+
21
+ export type ComponentPlatform = "web" | "mobile" | "all";
22
+
23
+ export const ComponentPlatform = {
24
+ WEB: "web" as ComponentPlatform,
25
+ MOBILE: "mobile" as ComponentPlatform,
26
+ ALL: "all" as ComponentPlatform
27
+ };
28
+
29
+ interface ComponentHeader {
30
+ name: string;
31
+ version?: string;
32
+ release: string;
33
+ tags: string[];
34
+ description: string;
35
+ }
36
+
37
+ export interface ComponentSummary extends ComponentHeader {
38
+ updatedOn: Date;
39
+ }
40
+
41
+ export interface ComponentMeta extends ComponentHeader {
42
+ dependencies: Record<string, string>;
43
+ devDependencies: Record<string, string>;
44
+ internalDependencies: string[];
45
+ }
46
+
47
+ export interface ComponentFile {
48
+ name: string;
49
+ content: string;
50
+ checksum?: string;
51
+ updatedOn: Date;
52
+ }
53
+
54
+ export interface ComponentDetails extends ComponentMeta {
55
+ files: ComponentFile[];
56
+ }
57
+
58
+ export const HttpHeaders = {
59
+ Version: "x-cyclone-version",
60
+ Checksum: "x-cyclone-checksum"
61
+ };
62
+
63
+ export interface Env {
64
+ STORAGE_BUCKET: RegistryStorage;
65
+ }
66
+
67
+ export interface RegistryStorage {
68
+ list(options: { prefix: string; cursor?: string }): Promise<R2Objects>;
69
+ get(key: string): Promise<R2ObjectBody | null>;
70
+ }
@@ -0,0 +1,96 @@
1
+ import type { R2Object, R2ObjectBody } from "@cloudflare/workers-types";
2
+ import { describe, expect, it, vi } from "vitest";
3
+ import type { Context } from "../../src/context";
4
+ import { router } from "../../src/router";
5
+ import type { ComponentMeta, RegistryStorage } from "../../src/types";
6
+
7
+ function object(key: string, uploaded: string): R2Object {
8
+ return { key, uploaded: new Date(uploaded) } as R2Object;
9
+ }
10
+
11
+ function body(value: ComponentMeta | string): R2ObjectBody {
12
+ return {
13
+ json: vi.fn(async () => value),
14
+ text: vi.fn(async () =>
15
+ typeof value === "string" ? value : JSON.stringify(value)
16
+ )
17
+ } as unknown as R2ObjectBody;
18
+ }
19
+
20
+ function meta(version: string): ComponentMeta {
21
+ return {
22
+ name: "button",
23
+ version,
24
+ release: "latest",
25
+ description: "A button.",
26
+ tags: ["form"],
27
+ dependencies: {},
28
+ devDependencies: {},
29
+ internalDependencies: []
30
+ };
31
+ }
32
+
33
+ function context(
34
+ objects: R2Object[],
35
+ contents: Record<string, R2ObjectBody>
36
+ ): Context {
37
+ return {
38
+ version: undefined,
39
+ storage: {
40
+ list: vi.fn(async () => ({
41
+ objects,
42
+ truncated: false,
43
+ delimitedPrefixes: []
44
+ })),
45
+ get: vi.fn(async (key: string) => contents[key] ?? null)
46
+ } as unknown as RegistryStorage
47
+ } as Context;
48
+ }
49
+
50
+ describe("components router", () => {
51
+ it("lists only the newest uploaded version of each component", async () => {
52
+ const oldMeta = "registry/components/button/1.0.0/meta.json";
53
+ const newMeta = "registry/components/button/2.0.0/meta.json";
54
+ const caller = router.createCaller(
55
+ context(
56
+ [
57
+ object(oldMeta, "2026-01-01T00:00:00Z"),
58
+ object(newMeta, "2026-02-01T00:00:00Z")
59
+ ],
60
+ { [oldMeta]: body(meta("1.0.0")), [newMeta]: body(meta("2.0.0")) }
61
+ )
62
+ );
63
+
64
+ await expect(caller.components.list()).resolves.toMatchObject([
65
+ { name: "button", version: "2.0.0" }
66
+ ]);
67
+ });
68
+
69
+ it("returns files only from the newest component version", async () => {
70
+ const oldMeta = "registry/components/button/1.0.0/meta.json";
71
+ const oldFile = "registry/components/button/1.0.0/index.ts";
72
+ const newMeta = "registry/components/button/2.0.0/meta.json";
73
+ const newFile = "registry/components/button/2.0.0/index.ts";
74
+ const caller = router.createCaller(
75
+ context(
76
+ [
77
+ object(oldMeta, "2026-01-01T00:00:00Z"),
78
+ object(oldFile, "2026-01-01T00:00:00Z"),
79
+ object(newMeta, "2026-02-01T00:00:00Z"),
80
+ object(newFile, "2026-02-01T00:00:00Z")
81
+ ],
82
+ {
83
+ [oldMeta]: body(meta("1.0.0")),
84
+ [oldFile]: body("old"),
85
+ [newMeta]: body(meta("2.0.0")),
86
+ [newFile]: body("new")
87
+ }
88
+ )
89
+ );
90
+
91
+ await expect(caller.components.get("button")).resolves.toMatchObject({
92
+ version: "2.0.0",
93
+ files: [{ name: "index.ts", content: "new" }]
94
+ });
95
+ });
96
+ });
package/tsconfig.json ADDED
@@ -0,0 +1,13 @@
1
+ {
2
+ "extends": "../../tsconfig.base.json",
3
+ "compilerOptions": {
4
+ "tsBuildInfoFile": "../../dist/out-tsc/packages/registry-api/tsconfig.tsbuildinfo",
5
+ "types": ["@cloudflare/workers-types"],
6
+ "strict": true,
7
+ "declaration": true,
8
+ "outDir": "../../dist/out-tsc/packages/registry-api",
9
+ "ignoreDeprecations": "6.0"
10
+ },
11
+ "include": ["src/**/*", "powerlines.config.ts", "powerlines.d.ts"],
12
+ "exclude": ["vitest.config.mts", "src/**/*.spec.ts", "src/**/*.test.ts"]
13
+ }
@@ -0,0 +1,16 @@
1
+ {
2
+ "extends": "./tsconfig.json",
3
+ "compilerOptions": {
4
+ "rootDir": ".",
5
+ "types": [
6
+ "vitest/globals",
7
+ "vitest/importMeta",
8
+ "node",
9
+ "vitest",
10
+ "@cloudflare/workers-types"
11
+ ],
12
+ "outDir": "../../dist/out-tsc"
13
+ },
14
+ "include": ["vitest.config.mts", "src/**/*.ts", "src/**/*.d.ts"],
15
+ "exclude": []
16
+ }
@@ -0,0 +1,24 @@
1
+ import { nxViteTsPaths } from "@nx/vite/plugins/nx-tsconfig-paths.plugin";
2
+ import { defineConfig } from "vitest/config";
3
+
4
+ export default defineConfig(() => ({
5
+ root: import.meta.dirname,
6
+ cacheDir: "../../node_modules/.vite/packages/registry-api",
7
+ plugins: [nxViteTsPaths()],
8
+ test: {
9
+ name: "registry-api",
10
+ watch: false,
11
+ globals: true,
12
+ environment: "node",
13
+ include: [
14
+ "src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}",
15
+ "test/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}",
16
+ "tests/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}"
17
+ ],
18
+ reporters: ["default"],
19
+ coverage: {
20
+ reportsDirectory: "../../coverage/packages/registry-api",
21
+ provider: "v8" as const
22
+ }
23
+ }
24
+ }));