@content-collections/core 0.1.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.
Files changed (87) hide show
  1. package/.turbo/turbo-build.log +14 -0
  2. package/.turbo/turbo-test.log +47 -0
  3. package/.turbo/turbo-typecheck.log +4 -0
  4. package/LICENSE +21 -0
  5. package/coverage/.tmp/coverage-0.json +1 -0
  6. package/coverage/.tmp/coverage-1.json +1 -0
  7. package/coverage/.tmp/coverage-2.json +1 -0
  8. package/coverage/.tmp/coverage-3.json +1 -0
  9. package/coverage/.tmp/coverage-4.json +1 -0
  10. package/coverage/.tmp/coverage-5.json +1 -0
  11. package/coverage/.tmp/coverage-6.json +1 -0
  12. package/coverage/.tmp/coverage-7.json +1 -0
  13. package/coverage/.tmp/coverage-8.json +1 -0
  14. package/coverage/.tmp/coverage-9.json +1 -0
  15. package/coverage/base.css +224 -0
  16. package/coverage/block-navigation.js +87 -0
  17. package/coverage/builder.ts.html +424 -0
  18. package/coverage/clover.xml +960 -0
  19. package/coverage/collector.ts.html +427 -0
  20. package/coverage/config.ts.html +403 -0
  21. package/coverage/configurationReader.ts.html +409 -0
  22. package/coverage/coverage-final.json +11 -0
  23. package/coverage/events.ts.html +310 -0
  24. package/coverage/favicon.png +0 -0
  25. package/coverage/index.html +251 -0
  26. package/coverage/index.ts.html +106 -0
  27. package/coverage/prettify.css +1 -0
  28. package/coverage/prettify.js +2 -0
  29. package/coverage/sort-arrow-sprite.png +0 -0
  30. package/coverage/sorter.js +196 -0
  31. package/coverage/synchronizer.ts.html +394 -0
  32. package/coverage/transformer.ts.html +607 -0
  33. package/coverage/utils.ts.html +118 -0
  34. package/coverage/writer.ts.html +424 -0
  35. package/dist/index.cjs +416 -0
  36. package/dist/index.d.cts +59 -0
  37. package/dist/index.d.ts +146 -0
  38. package/dist/index.js +630 -0
  39. package/package.json +39 -0
  40. package/src/__tests__/.content-collections/cache/config.001.ts.js +19 -0
  41. package/src/__tests__/.content-collections/cache/config.002.mjs +16 -0
  42. package/src/__tests__/.content-collections/cache/config.002.ts.js +16 -0
  43. package/src/__tests__/.content-collections/cache/config.003.ts.js +24 -0
  44. package/src/__tests__/.content-collections/cache/config.004.mjs +47 -0
  45. package/src/__tests__/.content-collections/different-cache-dir/different.config.js +19 -0
  46. package/src/__tests__/.content-collections/generated-config.002/allPosts.json +13 -0
  47. package/src/__tests__/.content-collections/generated-config.002/index.d.ts +7 -0
  48. package/src/__tests__/.content-collections/generated-config.002/index.js +5 -0
  49. package/src/__tests__/.content-collections/generated-config.004/allAuthors.json +13 -0
  50. package/src/__tests__/.content-collections/generated-config.004/allPosts.json +13 -0
  51. package/src/__tests__/.content-collections/generated-config.004/index.d.ts +10 -0
  52. package/src/__tests__/.content-collections/generated-config.004/index.js +6 -0
  53. package/src/__tests__/collections/posts.ts +15 -0
  54. package/src/__tests__/config.001.ts +19 -0
  55. package/src/__tests__/config.002.ts +14 -0
  56. package/src/__tests__/config.003.ts +6 -0
  57. package/src/__tests__/config.004.ts +47 -0
  58. package/src/__tests__/invalid +1 -0
  59. package/src/__tests__/sources/authors/trillian.md +8 -0
  60. package/src/__tests__/sources/posts/first.md +6 -0
  61. package/src/__tests__/sources/test/001.md +5 -0
  62. package/src/__tests__/sources/test/002.md +5 -0
  63. package/src/__tests__/sources/test/broken-frontmatter +6 -0
  64. package/src/builder.test.ts +180 -0
  65. package/src/builder.ts +113 -0
  66. package/src/collector.test.ts +157 -0
  67. package/src/collector.ts +114 -0
  68. package/src/config.ts +106 -0
  69. package/src/configurationReader.test.ts +104 -0
  70. package/src/configurationReader.ts +108 -0
  71. package/src/events.test.ts +84 -0
  72. package/src/events.ts +75 -0
  73. package/src/index.ts +7 -0
  74. package/src/synchronizer.test.ts +192 -0
  75. package/src/synchronizer.ts +103 -0
  76. package/src/transformer.test.ts +431 -0
  77. package/src/transformer.ts +174 -0
  78. package/src/types.test.ts +137 -0
  79. package/src/types.ts +33 -0
  80. package/src/utils.test.ts +48 -0
  81. package/src/utils.ts +11 -0
  82. package/src/watcher.test.ts +200 -0
  83. package/src/watcher.ts +56 -0
  84. package/src/writer.test.ts +135 -0
  85. package/src/writer.ts +113 -0
  86. package/tsconfig.json +27 -0
  87. package/vite.config.ts +24 -0
@@ -0,0 +1,157 @@
1
+ import { describe, it, expect, beforeEach } from "vitest";
2
+ import { createCollector } from "./collector";
3
+ import { Events, createEmitter } from "./events";
4
+
5
+ describe("collector", () => {
6
+ let emitter = createEmitter<Events>();
7
+
8
+ beforeEach(() => {
9
+ emitter = createEmitter<Events>();
10
+ });
11
+
12
+ describe("collectFile", () => {
13
+ it("should collect file", async () => {
14
+ const { collectFile } = createCollector(emitter);
15
+
16
+ const file = await collectFile(
17
+ __dirname,
18
+ "./__tests__/sources/test/001.md"
19
+ );
20
+
21
+ if (!file) {
22
+ throw new Error("File not found");
23
+ }
24
+
25
+ expect(file.path).toBe("./__tests__/sources/test/001.md");
26
+ expect(file.data.content.trim()).toBe("# One");
27
+ expect(file.data.name).toBe("One");
28
+ });
29
+
30
+ it("should throw an error if file does not exist", async () => {
31
+ emitter.on("collector:read-error", ({ error }) => {
32
+ throw error;
33
+ });
34
+ const { collectFile } = createCollector(emitter);
35
+
36
+ await expect(
37
+ collectFile(__dirname, "./__tests__/sources/test/notfound.md")
38
+ ).rejects.toThrow("no such file or directory");
39
+ });
40
+
41
+ it("should capture the error and return null", async () => {
42
+ emitter.on("collector:read-error", ({ error }) => {
43
+ expect(error.type).toBe("Read");
44
+ expect(error.message).toMatch(/no such file or directory/);
45
+ });
46
+
47
+ const { collectFile } = createCollector(emitter, __dirname);
48
+ const file = await collectFile(
49
+ __dirname,
50
+ "./__tests__/sources/test/notfound.md"
51
+ );
52
+ expect(file).toBeNull();
53
+ });
54
+
55
+ it("should capture the parse error", async () => {
56
+ emitter.on("collector:parse-error", ({ error }) => {
57
+ expect(error.type).toBe("Parse");
58
+ expect(error.message).toMatch(/end of the stream/);
59
+ });
60
+
61
+ const { collectFile } = createCollector(emitter, __dirname);
62
+ const file = await collectFile(
63
+ __dirname,
64
+ "./__tests__/sources/test/broken-frontmatter"
65
+ );
66
+ expect(file).toBeNull();
67
+ });
68
+ });
69
+
70
+ describe("collect", () => {
71
+ const { collect } = createCollector(emitter, __dirname);
72
+
73
+ it("should collect one collection", async () => {
74
+ const collections = await collect([
75
+ {
76
+ directory: "./__tests__/sources/test/",
77
+ include: "*.md",
78
+ },
79
+ ]);
80
+
81
+ expect(collections).toHaveLength(1);
82
+ });
83
+
84
+ it("should collect two files", async () => {
85
+ const [collection] = await collect([
86
+ {
87
+ directory: "./__tests__/sources/test/",
88
+ include: "*.md",
89
+ },
90
+ ]);
91
+
92
+ expect(collection?.files).toHaveLength(2);
93
+ });
94
+
95
+ it("should store file path", async () => {
96
+ const [collection] = await collect([
97
+ {
98
+ directory: "./__tests__/sources/test/",
99
+ include: "*.md",
100
+ },
101
+ ]);
102
+
103
+ expect(collection?.files[0]?.path).toMatch(/001\.md$/);
104
+ expect(collection?.files[1]?.path).toMatch(/002\.md$/);
105
+ });
106
+
107
+ it("should parse frontmatter", async () => {
108
+ const [collection] = await collect([
109
+ {
110
+ directory: "./__tests__/sources/test/",
111
+ include: "*.md",
112
+ },
113
+ ]);
114
+
115
+ expect(collection?.files[0]?.data.name).toBe("One");
116
+ expect(collection?.files[1]?.data.name).toBe("Two");
117
+ });
118
+
119
+ it("should store body", async () => {
120
+ const [collection] = await collect([
121
+ {
122
+ directory: "./__tests__/sources/test/",
123
+ include: "*.md",
124
+ },
125
+ ]);
126
+
127
+ expect(collection?.files[0]?.data.content.trim()).toBe("# One");
128
+ expect(collection?.files[1]?.data.content.trim()).toBe("# Two");
129
+ });
130
+
131
+ it("should collect single collection from multiple sources", async () => {
132
+ const [collection] = await collect([
133
+ {
134
+ directory: "./__tests__/sources/test",
135
+ include: ["001.md", "002.md"],
136
+ },
137
+ ]);
138
+
139
+ expect(collection?.files).toHaveLength(2);
140
+ });
141
+
142
+ it("should collect multiple collections", async () => {
143
+ const collections = await collect([
144
+ {
145
+ directory: "./__tests__/sources/test/",
146
+ include: "*.md",
147
+ },
148
+ {
149
+ directory: "./__tests__/sources/posts/",
150
+ include: "*.md",
151
+ },
152
+ ]);
153
+
154
+ expect(collections).toHaveLength(2);
155
+ });
156
+ });
157
+ });
@@ -0,0 +1,114 @@
1
+ import matter from "gray-matter";
2
+ import fg from "fast-glob";
3
+ import { readFile } from "fs/promises";
4
+ import { AnyCollection } from "./config";
5
+ import path from "path";
6
+ import { isDefined } from "./utils";
7
+ import { CollectionFile } from "./types";
8
+ import { Emitter } from "./events";
9
+
10
+ export type CollectorEvents = {
11
+ "collector:read-error": {
12
+ filePath: string;
13
+ error: CollectError;
14
+ };
15
+ "collector:parse-error": {
16
+ filePath: string;
17
+ error: CollectError;
18
+ };
19
+ }
20
+
21
+ export type ErrorType = "Parse" | "Read";
22
+
23
+ export class CollectError extends Error {
24
+ type: ErrorType;
25
+ constructor(type: ErrorType, message: string) {
26
+ super(message);
27
+ this.type = type;
28
+ }
29
+ }
30
+
31
+ type FileCollection = Pick<AnyCollection, "directory" | "include">;
32
+
33
+ export function createCollector(emitter: Emitter, baseDirectory: string = ".") {
34
+ async function read(filePath: string) {
35
+ try {
36
+ return await readFile(filePath, "utf-8");
37
+ } catch (error) {
38
+ emitter.emit("collector:read-error", {
39
+ filePath,
40
+ error: new CollectError("Read", String(error)),
41
+ });
42
+ return null;
43
+ }
44
+ }
45
+
46
+ function parse(file: string) {
47
+ const { data, content } = matter(file);
48
+
49
+ return {
50
+ ...data,
51
+ content
52
+ };
53
+ }
54
+
55
+ async function collectFile(
56
+ directory: string,
57
+ filePath: string
58
+ ): Promise<CollectionFile | null> {
59
+ const file = await read(path.join(directory, filePath));
60
+ if (!file) {
61
+ return null;
62
+ }
63
+
64
+ try {
65
+ const data = parse(file);
66
+
67
+ return {
68
+ data,
69
+ path: filePath,
70
+ };
71
+ } catch (error) {
72
+ emitter.emit("collector:parse-error", {
73
+ filePath: path.join(directory, filePath),
74
+ error: new CollectError("Parse", String(error)),
75
+ });
76
+ return null;
77
+ }
78
+ }
79
+
80
+ async function resolveCollection<T extends FileCollection>(collection: T) {
81
+ const collectionDirectory = path.join(baseDirectory, collection.directory);
82
+ const filePaths = await fg(collection.include, {
83
+ cwd: collectionDirectory,
84
+ onlyFiles: true,
85
+ absolute: false,
86
+ });
87
+ const promises = filePaths.map((filePath) =>
88
+ collectFile(collectionDirectory, filePath)
89
+ );
90
+
91
+ const files = await Promise.all(promises);
92
+
93
+ return {
94
+ ...collection,
95
+ files: files.filter(isDefined),
96
+ };
97
+ }
98
+
99
+ async function collect<T extends FileCollection>(
100
+ unresolvedCollections: Array<T>
101
+ ) {
102
+ const promises = unresolvedCollections.map((collection) =>
103
+ resolveCollection(collection)
104
+ );
105
+ return await Promise.all(promises);
106
+ }
107
+
108
+ return {
109
+ collect,
110
+ collectFile,
111
+ };
112
+ }
113
+
114
+ export type Collector = ReturnType<typeof createCollector>;
package/src/config.ts ADDED
@@ -0,0 +1,106 @@
1
+ import { ZodObject, ZodRawShape, ZodString, ZodTypeAny, z } from "zod";
2
+ import { generateTypeName } from "./utils";
3
+
4
+ export type Meta = {
5
+ filePath: string;
6
+ fileName: string;
7
+ directory: string;
8
+ path: string;
9
+ extension: string;
10
+ };
11
+
12
+ type WithContent = {
13
+ content: ZodString;
14
+ };
15
+
16
+ type AddContent<TShape extends ZodRawShape> = TShape extends {
17
+ content: ZodTypeAny;
18
+ }
19
+ ? TShape
20
+ : TShape & WithContent;
21
+
22
+ export type Schema<TShape extends ZodRawShape> = z.infer<
23
+ ZodObject<AddContent<TShape>>
24
+ > & {
25
+ _meta: Meta;
26
+ };
27
+
28
+ export type Context = {
29
+ documents<TCollection extends AnyCollection>(
30
+ collection: TCollection
31
+ ): Array<Schema<TCollection["schema"]>>;
32
+ };
33
+
34
+ type Z = typeof z;
35
+
36
+ export type CollectionRequest<
37
+ TName extends string,
38
+ TShape extends ZodRawShape,
39
+ TSchema,
40
+ TTransformResult,
41
+ TDocument
42
+ > = {
43
+ name: TName;
44
+ typeName?: string;
45
+ schema: (z: Z) => TShape;
46
+ transform?: (context: Context, data: TSchema) => TTransformResult;
47
+ directory: string;
48
+ include: string | string[];
49
+ onSuccess?: (documents: Array<TDocument>) => void | Promise<void>;
50
+ };
51
+
52
+ export type Collection<
53
+ TName extends string,
54
+ TShape extends ZodRawShape,
55
+ TSchema,
56
+ TTransformResult,
57
+ TDocument
58
+ > = Omit<
59
+ CollectionRequest<TName, TShape, TSchema, TTransformResult, TDocument>,
60
+ "schema"
61
+ > & {
62
+ typeName: string;
63
+ schema: TShape;
64
+ };
65
+
66
+ export type AnyCollection = Collection<any, ZodRawShape, any, any, any>;
67
+
68
+ export function defineCollection<
69
+ TName extends string,
70
+ TShape extends ZodRawShape,
71
+ TSchema = Schema<TShape>,
72
+ TTransformResult = never,
73
+ TDocument = [TTransformResult] extends [never]
74
+ ? Schema<TShape>
75
+ : Awaited<TTransformResult>
76
+ >(
77
+ collection: CollectionRequest<
78
+ TName,
79
+ TShape,
80
+ TSchema,
81
+ TTransformResult,
82
+ TDocument
83
+ >
84
+ ): Collection<TName, TShape, TSchema, TTransformResult, TDocument> {
85
+ let typeName = collection.typeName;
86
+ if (!typeName) {
87
+ typeName = generateTypeName(collection.name);
88
+ }
89
+ return {
90
+ ...collection,
91
+ typeName,
92
+ schema: collection.schema(z),
93
+ };
94
+ }
95
+
96
+ export type Configuration<TCollections extends Array<AnyCollection>> = {
97
+ collections: TCollections;
98
+ };
99
+
100
+ export type AnyConfiguration = Configuration<Array<AnyCollection>>;
101
+
102
+ export function defineConfig<TConfig extends AnyConfiguration>(
103
+ config: TConfig
104
+ ) {
105
+ return config;
106
+ }
@@ -0,0 +1,104 @@
1
+ import path from "path";
2
+ import { describe, it, expect } from "vitest";
3
+ import { createConfigurationReader } from "./configurationReader";
4
+ import { existsSync } from "fs";
5
+ import { z } from "zod";
6
+
7
+ function config(name: string) {
8
+ const configPath = path.join(__dirname, "__tests__", name);
9
+ const readConfig = createConfigurationReader();
10
+ return readConfig(configPath, { configName: name + ".js" });
11
+ }
12
+
13
+ describe("configurationReader", () => {
14
+ it("should read the config", async () => {
15
+ const cfg = await config("config.001.ts");
16
+
17
+ expect(cfg.collections).toHaveLength(1);
18
+ });
19
+
20
+ it("should have a typeName", async () => {
21
+ const cfg = await config("config.001.ts");
22
+
23
+ expect(cfg.collections).toHaveLength(1);
24
+ expect(cfg.collections[0]?.typeName).toBe("Post");
25
+ });
26
+
27
+ it("should have a directory", async () => {
28
+ const cfg = await config("config.001.ts");
29
+
30
+ expect(cfg.collections).toHaveLength(1);
31
+ expect(cfg.collections[0]?.directory).toBe("sources/posts");
32
+ });
33
+
34
+ it("should have a include pattern", async () => {
35
+ const cfg = await config("config.001.ts");
36
+
37
+ expect(cfg.collections).toHaveLength(1);
38
+ expect(cfg.collections[0]?.include).toBe("**/*.md(x)?");
39
+ });
40
+
41
+ it("should generate a typeName", async () => {
42
+ const cfg = await config("config.002.ts");
43
+
44
+ expect(cfg.collections).toHaveLength(1);
45
+ expect(cfg.collections[0]?.typeName).toBe("Post");
46
+ });
47
+
48
+ it("should read the config with an imported collection", async () => {
49
+ const cfg = await config("config.003.ts");
50
+
51
+ expect(cfg.collections).toHaveLength(1);
52
+ expect(cfg.collections[0]?.name).toBe("posts");
53
+ });
54
+
55
+ it("should have a valid zod schema", async () => {
56
+ const cfg = await config("config.002.ts");
57
+ const schema = cfg.collections[0]?.schema;
58
+ if (!schema) {
59
+ throw new Error("collection does not have a schema");
60
+ }
61
+
62
+ let result = z.object(schema).safeParse({
63
+ title: "Hello",
64
+ });
65
+ expect(result.success).toBe(true);
66
+
67
+ result = z.object(schema).safeParse({
68
+ greeting: "Hello",
69
+ });
70
+ expect(result.success).toBe(false);
71
+ });
72
+
73
+ it("should use different cache directory and name", async () => {
74
+ const basedir = path.join(__dirname, "__tests__");
75
+ const cacheDir = path.join(
76
+ basedir,
77
+ ".content-collections",
78
+ "different-cache-dir"
79
+ );
80
+ const configPath = path.join(basedir, "config.001.ts");
81
+ const configName = "different.config.js";
82
+ const readConfig = createConfigurationReader();
83
+ const cfg = await readConfig(configPath, {
84
+ configName,
85
+ cacheDir,
86
+ });
87
+
88
+ const compiledConfig = path.join(cacheDir, configName);
89
+ expect(cfg.collections).toHaveLength(1);
90
+ expect(existsSync(compiledConfig)).toBe(true);
91
+ });
92
+
93
+ it("should throw an error if the config file does not exists", async () => {
94
+ await expect(config("non-existing")).rejects.toThrowError(
95
+ /configuration file .*\/non-existing does not exist/
96
+ );
97
+ });
98
+
99
+ it("should throw an error if the config file is invalid", async () => {
100
+ await expect(config("invalid")).rejects.toThrowError(
101
+ /configuration file .*\/invalid is invalid/
102
+ );
103
+ });
104
+ });
@@ -0,0 +1,108 @@
1
+ import * as esbuild from "esbuild";
2
+ import fs from "node:fs/promises";
3
+ import path from "node:path";
4
+ import packageJson from "../package.json";
5
+ import { AnyCollection } from "./config";
6
+ import { existsSync } from "node:fs";
7
+
8
+ export type ErrorType = "Read" | "Compile";
9
+
10
+ export class ConfigurationError extends Error {
11
+ type: ErrorType;
12
+ constructor(type: ErrorType, message: string) {
13
+ super(message);
14
+ this.type = type;
15
+ }
16
+ }
17
+
18
+ export type InternalConfiguration = {
19
+ collections: Array<AnyCollection>;
20
+ path: string;
21
+ generateTypes?: boolean;
22
+ };
23
+
24
+ const importPathPlugin: esbuild.Plugin = {
25
+ name: "import-path",
26
+ setup(build) {
27
+ build.onResolve({ filter: /^\@content-collections\/core$/ }, () => {
28
+ return { path: path.join(__dirname, "index.ts"), external: true };
29
+ });
30
+ },
31
+ };
32
+
33
+ export type Options = {
34
+ configName: string;
35
+ cacheDir?: string;
36
+ };
37
+
38
+ export const defaultConfigName = "content-collection-config.mjs";
39
+
40
+ function resolveCacheDir(config: string, options: Options) {
41
+ if (options.cacheDir) {
42
+ return options.cacheDir;
43
+ }
44
+ return path.join(path.dirname(config), ".content-collections", "cache");
45
+ }
46
+
47
+ async function compile(configurationPath: string, outfile: string) {
48
+ const plugins: Array<esbuild.Plugin> = [];
49
+ if (process.env.NODE_ENV === "test") {
50
+ plugins.push(importPathPlugin);
51
+ }
52
+
53
+ await esbuild.build({
54
+ entryPoints: [configurationPath],
55
+ external: [...Object.keys(packageJson.dependencies), "@content-collections/*"],
56
+ bundle: true,
57
+ platform: "node",
58
+ format: "esm",
59
+ plugins,
60
+ outfile,
61
+ });
62
+ }
63
+
64
+ // errorHandler does not make sense here:
65
+ // because if the configuration is invalid, the program should exit
66
+
67
+ export function createConfigurationReader() {
68
+ return async (
69
+ configurationPath: string,
70
+ options: Options = {
71
+ configName: defaultConfigName,
72
+ }
73
+ ): Promise<InternalConfiguration> => {
74
+ if (!existsSync(configurationPath)) {
75
+ throw new ConfigurationError(
76
+ "Read",
77
+ `configuration file ${configurationPath} does not exist`
78
+ );
79
+ }
80
+
81
+ const cacheDir = resolveCacheDir(configurationPath, options);
82
+ await fs.mkdir(cacheDir, { recursive: true });
83
+
84
+ const outfile = path.join(cacheDir, options.configName);
85
+
86
+ try {
87
+ await compile(configurationPath, outfile);
88
+ } catch (error) {
89
+ throw new ConfigurationError(
90
+ "Compile",
91
+ `configuration file ${configurationPath} is invalid: ${error}`
92
+ );
93
+ }
94
+
95
+ const module = await import(
96
+ `file://${path.resolve(outfile)}?x=${Date.now()}`
97
+ );
98
+ return {
99
+ ...module.default,
100
+ path: configurationPath,
101
+ generateTypes: true,
102
+ };
103
+ };
104
+ }
105
+
106
+ export type ConfigurationReader = Awaited<
107
+ ReturnType<typeof createConfigurationReader>
108
+ >;
@@ -0,0 +1,84 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { createEmitter } from "./events";
3
+
4
+ type Events = {
5
+ test: {
6
+ a: string;
7
+ };
8
+ other: {
9
+ error: Error
10
+ }
11
+ };
12
+
13
+ describe("events", () => {
14
+ it("should emit and listen to events", () => {
15
+ const emitter = createEmitter<Events>();
16
+ emitter.on("test", (event) => {
17
+ expect(event.a).toBe("test");
18
+ });
19
+ emitter.emit("test", { a: "test" });
20
+ });
21
+
22
+ it("should only allow keys from the event map", () => {
23
+ const emitter = createEmitter<Events>();
24
+ // @ts-expect-error test 2 does not exist
25
+ emitter.emit("test2", { a: "test" });
26
+ });
27
+
28
+ it("should only allow listeners for keys from the event map", () => {
29
+ const emitter = createEmitter<Events>();
30
+ // @ts-expect-error test 2 does not exist
31
+ emitter.on("test2", (event) => {});
32
+ });
33
+
34
+ it("should only correct event type on emit", () => {
35
+ const emitter = createEmitter<Events>();
36
+ // @ts-expect-error b does not exist
37
+ emitter.emit("test", { b: "test" });
38
+ });
39
+
40
+ it("should only allow listeners with the correct event type", () => {
41
+ const emitter = createEmitter<Events>();
42
+ emitter.on("test", (event) => {
43
+ // @ts-expect-error b does not exist
44
+ expect(event.b).toBeUndefined();
45
+ });
46
+ });
47
+
48
+ it("should emit extra collection-error on error event", () => {
49
+ const emitter = createEmitter<Events>();
50
+
51
+ const events: Array<string> = [];
52
+
53
+ emitter.on("other", (event) => {
54
+ events.push("other");
55
+ });
56
+ emitter.on("_error", (event) => {
57
+ events.push("error");
58
+ expect(event._event).toBe("other");
59
+ expect(event.error.message).toBe("test");
60
+ });
61
+
62
+ emitter.emit("other", { error: new Error("test") });
63
+
64
+ expect(events).toEqual(["other", "error"]);
65
+ });
66
+
67
+ it("should emit extra _all event", () => {
68
+ const emitter = createEmitter<Events>();
69
+
70
+ const events: Array<string> = [];
71
+
72
+ emitter.on("test", (event) => {
73
+ events.push("test");
74
+ });
75
+ emitter.on("_all", (event) => {
76
+ events.push("all");
77
+ expect(event._event).toBe("test");
78
+ });
79
+
80
+ emitter.emit("test", { a: "test" });
81
+
82
+ expect(events).toEqual(["test", "all"]);
83
+ });
84
+ });