@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.
- package/.turbo/turbo-build.log +14 -0
- package/.turbo/turbo-test.log +47 -0
- package/.turbo/turbo-typecheck.log +4 -0
- package/LICENSE +21 -0
- package/coverage/.tmp/coverage-0.json +1 -0
- package/coverage/.tmp/coverage-1.json +1 -0
- package/coverage/.tmp/coverage-2.json +1 -0
- package/coverage/.tmp/coverage-3.json +1 -0
- package/coverage/.tmp/coverage-4.json +1 -0
- package/coverage/.tmp/coverage-5.json +1 -0
- package/coverage/.tmp/coverage-6.json +1 -0
- package/coverage/.tmp/coverage-7.json +1 -0
- package/coverage/.tmp/coverage-8.json +1 -0
- package/coverage/.tmp/coverage-9.json +1 -0
- package/coverage/base.css +224 -0
- package/coverage/block-navigation.js +87 -0
- package/coverage/builder.ts.html +424 -0
- package/coverage/clover.xml +960 -0
- package/coverage/collector.ts.html +427 -0
- package/coverage/config.ts.html +403 -0
- package/coverage/configurationReader.ts.html +409 -0
- package/coverage/coverage-final.json +11 -0
- package/coverage/events.ts.html +310 -0
- package/coverage/favicon.png +0 -0
- package/coverage/index.html +251 -0
- package/coverage/index.ts.html +106 -0
- package/coverage/prettify.css +1 -0
- package/coverage/prettify.js +2 -0
- package/coverage/sort-arrow-sprite.png +0 -0
- package/coverage/sorter.js +196 -0
- package/coverage/synchronizer.ts.html +394 -0
- package/coverage/transformer.ts.html +607 -0
- package/coverage/utils.ts.html +118 -0
- package/coverage/writer.ts.html +424 -0
- package/dist/index.cjs +416 -0
- package/dist/index.d.cts +59 -0
- package/dist/index.d.ts +146 -0
- package/dist/index.js +630 -0
- package/package.json +39 -0
- package/src/__tests__/.content-collections/cache/config.001.ts.js +19 -0
- package/src/__tests__/.content-collections/cache/config.002.mjs +16 -0
- package/src/__tests__/.content-collections/cache/config.002.ts.js +16 -0
- package/src/__tests__/.content-collections/cache/config.003.ts.js +24 -0
- package/src/__tests__/.content-collections/cache/config.004.mjs +47 -0
- package/src/__tests__/.content-collections/different-cache-dir/different.config.js +19 -0
- package/src/__tests__/.content-collections/generated-config.002/allPosts.json +13 -0
- package/src/__tests__/.content-collections/generated-config.002/index.d.ts +7 -0
- package/src/__tests__/.content-collections/generated-config.002/index.js +5 -0
- package/src/__tests__/.content-collections/generated-config.004/allAuthors.json +13 -0
- package/src/__tests__/.content-collections/generated-config.004/allPosts.json +13 -0
- package/src/__tests__/.content-collections/generated-config.004/index.d.ts +10 -0
- package/src/__tests__/.content-collections/generated-config.004/index.js +6 -0
- package/src/__tests__/collections/posts.ts +15 -0
- package/src/__tests__/config.001.ts +19 -0
- package/src/__tests__/config.002.ts +14 -0
- package/src/__tests__/config.003.ts +6 -0
- package/src/__tests__/config.004.ts +47 -0
- package/src/__tests__/invalid +1 -0
- package/src/__tests__/sources/authors/trillian.md +8 -0
- package/src/__tests__/sources/posts/first.md +6 -0
- package/src/__tests__/sources/test/001.md +5 -0
- package/src/__tests__/sources/test/002.md +5 -0
- package/src/__tests__/sources/test/broken-frontmatter +6 -0
- package/src/builder.test.ts +180 -0
- package/src/builder.ts +113 -0
- package/src/collector.test.ts +157 -0
- package/src/collector.ts +114 -0
- package/src/config.ts +106 -0
- package/src/configurationReader.test.ts +104 -0
- package/src/configurationReader.ts +108 -0
- package/src/events.test.ts +84 -0
- package/src/events.ts +75 -0
- package/src/index.ts +7 -0
- package/src/synchronizer.test.ts +192 -0
- package/src/synchronizer.ts +103 -0
- package/src/transformer.test.ts +431 -0
- package/src/transformer.ts +174 -0
- package/src/types.test.ts +137 -0
- package/src/types.ts +33 -0
- package/src/utils.test.ts +48 -0
- package/src/utils.ts +11 -0
- package/src/watcher.test.ts +200 -0
- package/src/watcher.ts +56 -0
- package/src/writer.test.ts +135 -0
- package/src/writer.ts +113 -0
- package/tsconfig.json +27 -0
- package/vite.config.ts +24 -0
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { GetTypeByName } from "./types";
|
|
3
|
+
import { defineCollection, defineConfig } from "./config";
|
|
4
|
+
|
|
5
|
+
describe("types", () => {
|
|
6
|
+
describe("GetTypeByName", () => {
|
|
7
|
+
it("should infer type from schema", () => {
|
|
8
|
+
const collection = defineCollection({
|
|
9
|
+
name: "person",
|
|
10
|
+
typeName: "person",
|
|
11
|
+
directory: "./persons",
|
|
12
|
+
include: "*.md",
|
|
13
|
+
schema: (z) => ({
|
|
14
|
+
name: z.string(),
|
|
15
|
+
age: z.number(),
|
|
16
|
+
}),
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
const config = defineConfig({
|
|
20
|
+
collections: [collection],
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
type Person = GetTypeByName<typeof config, "person">;
|
|
24
|
+
|
|
25
|
+
const person: Person = {
|
|
26
|
+
name: "John",
|
|
27
|
+
age: 20,
|
|
28
|
+
// @ts-expect-error city is not in the schema
|
|
29
|
+
city: "New York",
|
|
30
|
+
_meta: {
|
|
31
|
+
fileName: "john.md",
|
|
32
|
+
filePath: "persons/john.md",
|
|
33
|
+
directory: "persons",
|
|
34
|
+
path: "persons/john",
|
|
35
|
+
extension: "md",
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
expect(person).toBeTruthy();
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("should infer type from transform function", () => {
|
|
43
|
+
const collection = defineCollection({
|
|
44
|
+
name: "person",
|
|
45
|
+
typeName: "person",
|
|
46
|
+
directory: "./persons",
|
|
47
|
+
include: "*.md",
|
|
48
|
+
schema: (z) => ({
|
|
49
|
+
name: z.string(),
|
|
50
|
+
age: z.number(),
|
|
51
|
+
}),
|
|
52
|
+
transform: (ctx, data) => {
|
|
53
|
+
return {
|
|
54
|
+
...data,
|
|
55
|
+
city: "New York",
|
|
56
|
+
};
|
|
57
|
+
},
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
const config = defineConfig({
|
|
61
|
+
collections: [collection],
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
type Person = GetTypeByName<typeof config, "person">;
|
|
65
|
+
const person: Person = {
|
|
66
|
+
name: "John",
|
|
67
|
+
city: "New York",
|
|
68
|
+
// @ts-expect-error street is not in the schema
|
|
69
|
+
street: "Main Street",
|
|
70
|
+
_meta: {
|
|
71
|
+
fileName: "john.md",
|
|
72
|
+
filePath: "persons/john.md",
|
|
73
|
+
directory: "persons",
|
|
74
|
+
path: "persons/john",
|
|
75
|
+
extension: "md",
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
expect(person).toBeTruthy();
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("should infer type from other collection", () => {
|
|
84
|
+
const countryCollection = defineCollection({
|
|
85
|
+
name: "country",
|
|
86
|
+
directory: "./countries",
|
|
87
|
+
include: "*.md",
|
|
88
|
+
schema: (z) => ({
|
|
89
|
+
code: z.string().length(2),
|
|
90
|
+
name: z.string(),
|
|
91
|
+
}),
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
const personCollection = defineCollection({
|
|
95
|
+
name: "person",
|
|
96
|
+
directory: "./persons",
|
|
97
|
+
include: "*.md",
|
|
98
|
+
schema: (z) => ({
|
|
99
|
+
name: z.string(),
|
|
100
|
+
age: z.number(),
|
|
101
|
+
countryCode: z.string().length(2),
|
|
102
|
+
}),
|
|
103
|
+
transform: (ctx, data) => {
|
|
104
|
+
const countries = ctx.documents(countryCollection);
|
|
105
|
+
const country = countries.find((c) => c.code === data.countryCode);
|
|
106
|
+
if (!country) {
|
|
107
|
+
throw new Error(`Country ${data.countryCode} not found`);
|
|
108
|
+
}
|
|
109
|
+
return {
|
|
110
|
+
name: data.name,
|
|
111
|
+
age: data.age,
|
|
112
|
+
country: {
|
|
113
|
+
code: country.code,
|
|
114
|
+
name: country.name,
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
},
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
const config = defineConfig({
|
|
121
|
+
collections: [countryCollection, personCollection],
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
type Person = GetTypeByName<typeof config, "person">;
|
|
125
|
+
|
|
126
|
+
const person: Person = {
|
|
127
|
+
name: "Hans",
|
|
128
|
+
age: 20,
|
|
129
|
+
country: {
|
|
130
|
+
code: "de",
|
|
131
|
+
name: "Germany",
|
|
132
|
+
},
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
expect(person).toBeTruthy();
|
|
136
|
+
});
|
|
137
|
+
});
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { ZodRawShape } from "zod";
|
|
2
|
+
import { AnyCollection, AnyConfiguration, Collection } from "./config";
|
|
3
|
+
|
|
4
|
+
export type Modification = "create" | "update" | "delete";
|
|
5
|
+
|
|
6
|
+
export type CollectionFile = {
|
|
7
|
+
data: {
|
|
8
|
+
content: string;
|
|
9
|
+
[key: string]: unknown;
|
|
10
|
+
};
|
|
11
|
+
path: string;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export type FileCollection = Pick<AnyCollection, "directory" | "include">;
|
|
15
|
+
|
|
16
|
+
export type ResolvedCollection<T extends FileCollection> = T & {
|
|
17
|
+
files: Array<CollectionFile>;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
type CollectionByName<TConfiguration extends AnyConfiguration> = {
|
|
21
|
+
[TCollection in TConfiguration["collections"][number] as TCollection["name"]]: TCollection;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
type GetDocument<TCollection extends AnyCollection> =
|
|
25
|
+
TCollection extends Collection<any, ZodRawShape, any, any, infer TDocument>
|
|
26
|
+
? TDocument
|
|
27
|
+
: never;
|
|
28
|
+
|
|
29
|
+
export type GetTypeByName<
|
|
30
|
+
TConfiguration extends AnyConfiguration,
|
|
31
|
+
TName extends keyof CollectionByName<TConfiguration>,
|
|
32
|
+
TCollection = CollectionByName<TConfiguration>[TName]
|
|
33
|
+
> = TCollection extends AnyCollection ? GetDocument<TCollection> : never;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { generateTypeName, isDefined } from "./utils";
|
|
3
|
+
|
|
4
|
+
describe("generateTypeName", () => {
|
|
5
|
+
|
|
6
|
+
it("should return same as collection name", () => {
|
|
7
|
+
const typeName = generateTypeName("Post");
|
|
8
|
+
expect(typeName).toBe("Post");
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it("should return typeName with first letter upper case", () => {
|
|
12
|
+
const typeName = generateTypeName("post");
|
|
13
|
+
expect(typeName).toBe("Post");
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it("should remove spaces", () => {
|
|
17
|
+
const typeName = generateTypeName("post title");
|
|
18
|
+
expect(typeName).toBe("PostTitle");
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("should return the singular form", () => {
|
|
22
|
+
const typeName = generateTypeName("posts");
|
|
23
|
+
expect(typeName).toBe("Post");
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
describe("isDefined", () => {
|
|
29
|
+
|
|
30
|
+
it("should filter null values", () => {
|
|
31
|
+
const values = [1, 2, null, 3, null];
|
|
32
|
+
const defined = values.filter(isDefined);
|
|
33
|
+
expect(defined).toEqual([1, 2, 3]);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("should filter undefined values", () => {
|
|
37
|
+
const values = [1, 2, undefined, 3, undefined];
|
|
38
|
+
const defined = values.filter(isDefined);
|
|
39
|
+
expect(defined).toEqual([1, 2, 3]);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("should filter null and undefined values", () => {
|
|
43
|
+
const values = [1, 2, undefined, 3, null];
|
|
44
|
+
const defined = values.filter(isDefined);
|
|
45
|
+
expect(defined).toEqual([1, 2, 3]);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
});
|
package/src/utils.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import camelcase from "camelcase";
|
|
2
|
+
import pluralize from "pluralize";
|
|
3
|
+
|
|
4
|
+
export function generateTypeName(name: string) {
|
|
5
|
+
const singularName = pluralize.singular(name);
|
|
6
|
+
return camelcase(singularName, { pascalCase: true });
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function isDefined<T>(value: T | undefined | null): value is T {
|
|
10
|
+
return value !== undefined && value !== null;
|
|
11
|
+
}
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import { existsSync } from "node:fs";
|
|
4
|
+
import { createWatcher } from "./watcher";
|
|
5
|
+
import { Modification } from "./types";
|
|
6
|
+
import { Events, createEmitter } from "./events";
|
|
7
|
+
|
|
8
|
+
const isEnabled = process.env.ENABLE_WATCHER_TESTS === "true";
|
|
9
|
+
|
|
10
|
+
describe.runIf(isEnabled)("watcher", () => {
|
|
11
|
+
const directories: Array<string> = [];
|
|
12
|
+
|
|
13
|
+
const events: Array<string> = [];
|
|
14
|
+
let build = false;
|
|
15
|
+
|
|
16
|
+
async function syncFn(
|
|
17
|
+
modification: Modification,
|
|
18
|
+
path: string
|
|
19
|
+
): Promise<boolean> {
|
|
20
|
+
events.push(`${modification}:${path}`);
|
|
21
|
+
return !path.endsWith("-ignore");
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async function buildFn() {
|
|
25
|
+
build = true;
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function findEvent(type: string, path: string, eventArray = events) {
|
|
30
|
+
return eventArray.find(
|
|
31
|
+
(event) => event.startsWith(`${type}:`) && event.endsWith(path)
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
let emitter = createEmitter<Events>();
|
|
36
|
+
|
|
37
|
+
beforeEach(() => {
|
|
38
|
+
emitter = createEmitter<Events>();
|
|
39
|
+
events.length = 0;
|
|
40
|
+
build = false;
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
afterEach(async () => {
|
|
44
|
+
for (const directory of directories.reverse()) {
|
|
45
|
+
if (existsSync(directory)) {
|
|
46
|
+
await fs.rm(directory, { recursive: true });
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
directories.length = 0;
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
const mkdir = async (path: string) => {
|
|
53
|
+
if (existsSync(path)) {
|
|
54
|
+
await fs.rm(path, { recursive: true });
|
|
55
|
+
}
|
|
56
|
+
await fs.mkdir(path);
|
|
57
|
+
|
|
58
|
+
directories.push(path);
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
it("should not build", async () => {
|
|
62
|
+
await mkdir("tmp");
|
|
63
|
+
|
|
64
|
+
const watcher = await createWatcher(emitter, ["tmp"], syncFn, buildFn);
|
|
65
|
+
|
|
66
|
+
await watcher.unsubscribe();
|
|
67
|
+
|
|
68
|
+
expect(build).toBe(false);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("should not build for untracked file", async () => {
|
|
72
|
+
await mkdir("tmp");
|
|
73
|
+
|
|
74
|
+
const watcher = await createWatcher(emitter, ["tmp"], syncFn, buildFn);
|
|
75
|
+
|
|
76
|
+
// we create one file and wait for the event
|
|
77
|
+
// because we receive often a event for the directory
|
|
78
|
+
await fs.writeFile("tmp/foo-ignore", "foo");
|
|
79
|
+
await vi.waitUntil(() => findEvent("create", "tmp/foo-ignore"));
|
|
80
|
+
|
|
81
|
+
// we reset the build flag and create a second file
|
|
82
|
+
build = false;
|
|
83
|
+
await fs.writeFile("tmp/bar-ignore", "foo");
|
|
84
|
+
await vi.waitUntil(() => findEvent("create", "tmp/bar-ignore"));
|
|
85
|
+
|
|
86
|
+
await watcher.unsubscribe();
|
|
87
|
+
|
|
88
|
+
expect(build).toBe(false);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("should emit create event", async () => {
|
|
92
|
+
await mkdir("tmp");
|
|
93
|
+
|
|
94
|
+
const watcher = await createWatcher(emitter, ["tmp"], syncFn, buildFn);
|
|
95
|
+
await fs.writeFile("tmp/foo", "foo");
|
|
96
|
+
|
|
97
|
+
await vi.waitUntil(() => findEvent("create", "tmp/foo"));
|
|
98
|
+
|
|
99
|
+
await watcher.unsubscribe();
|
|
100
|
+
|
|
101
|
+
expect(findEvent("create", "tmp/foo")).toBeTruthy();
|
|
102
|
+
expect(build).toBe(true);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("should emit update event", async () => {
|
|
106
|
+
await mkdir("tmp");
|
|
107
|
+
|
|
108
|
+
const watcher = await createWatcher(emitter, ["tmp"], syncFn, buildFn);
|
|
109
|
+
|
|
110
|
+
await fs.writeFile("tmp/foo", "foo", "utf-8");
|
|
111
|
+
await vi.waitUntil(() => findEvent("create", "tmp/foo"), 2000);
|
|
112
|
+
|
|
113
|
+
await fs.writeFile("tmp/foo", "bar", "utf-8");
|
|
114
|
+
await vi.waitUntil(() => findEvent("update", "tmp/foo"), 2000);
|
|
115
|
+
|
|
116
|
+
await watcher.unsubscribe();
|
|
117
|
+
|
|
118
|
+
expect(findEvent("update", "tmp/foo")).toBeTruthy();
|
|
119
|
+
expect(build).toBe(true);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it("should emit delete event", async () => {
|
|
123
|
+
await mkdir("tmp");
|
|
124
|
+
await fs.writeFile("tmp/foo", "foo");
|
|
125
|
+
|
|
126
|
+
const watcher = await createWatcher(emitter, ["tmp"], syncFn, buildFn);
|
|
127
|
+
await fs.rm("tmp/foo");
|
|
128
|
+
|
|
129
|
+
await vi.waitUntil(() => findEvent("delete", "tmp/foo"));
|
|
130
|
+
|
|
131
|
+
await watcher.unsubscribe();
|
|
132
|
+
|
|
133
|
+
expect(findEvent("delete", "tmp/foo")).toBeTruthy();
|
|
134
|
+
expect(build).toBe(true);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it("should emit events from multiple directories", async () => {
|
|
138
|
+
await mkdir("tmp-one");
|
|
139
|
+
await mkdir("tmp-two");
|
|
140
|
+
|
|
141
|
+
const watcher = await createWatcher(
|
|
142
|
+
emitter,
|
|
143
|
+
["tmp-one", "tmp-two"],
|
|
144
|
+
syncFn,
|
|
145
|
+
buildFn
|
|
146
|
+
);
|
|
147
|
+
await fs.writeFile("tmp-one/foo", "foo");
|
|
148
|
+
await fs.writeFile("tmp-two/bar", "bar");
|
|
149
|
+
|
|
150
|
+
await vi.waitUntil(
|
|
151
|
+
() =>
|
|
152
|
+
findEvent("create", "tmp-one/foo") && findEvent("create", "tmp-two/bar")
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
await watcher.unsubscribe();
|
|
156
|
+
|
|
157
|
+
expect(findEvent("create", "tmp-one/foo")).toBeTruthy();
|
|
158
|
+
expect(findEvent("create", "tmp-two/bar")).toBeTruthy();
|
|
159
|
+
expect(build).toBe(true);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it("should emit events from nested directories", async () => {
|
|
163
|
+
await mkdir("tmp");
|
|
164
|
+
await mkdir("tmp/foo");
|
|
165
|
+
await mkdir("tmp/bar");
|
|
166
|
+
|
|
167
|
+
const watcher = await createWatcher(emitter, ["tmp"], syncFn, buildFn);
|
|
168
|
+
await fs.writeFile("tmp/foo/baz", "baz");
|
|
169
|
+
await fs.writeFile("tmp/bar/qux", "qux");
|
|
170
|
+
|
|
171
|
+
await vi.waitUntil(
|
|
172
|
+
() =>
|
|
173
|
+
findEvent("create", "tmp/foo/baz") && findEvent("create", "tmp/bar/qux")
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
await watcher.unsubscribe();
|
|
177
|
+
|
|
178
|
+
expect(findEvent("create", "tmp/foo/baz")).toBeTruthy();
|
|
179
|
+
expect(findEvent("create", "tmp/bar/qux")).toBeTruthy();
|
|
180
|
+
expect(build).toBe(true);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it("should file change event to event emitter", async () => {
|
|
184
|
+
await mkdir("tmp");
|
|
185
|
+
|
|
186
|
+
const localEvents: Array<string> = [];
|
|
187
|
+
emitter.on("watcher:file-changed", ({ modification, filePath }) =>
|
|
188
|
+
localEvents.push(`${modification}:${filePath}`)
|
|
189
|
+
);
|
|
190
|
+
|
|
191
|
+
const watcher = await createWatcher(emitter, ["tmp"], syncFn, buildFn);
|
|
192
|
+
await fs.writeFile("tmp/foo", "foo");
|
|
193
|
+
|
|
194
|
+
await vi.waitUntil(() => findEvent("create", "tmp/foo", localEvents));
|
|
195
|
+
|
|
196
|
+
await watcher.unsubscribe();
|
|
197
|
+
|
|
198
|
+
expect(findEvent("create", "tmp/foo", localEvents)).toBeTruthy();
|
|
199
|
+
});
|
|
200
|
+
});
|
package/src/watcher.ts
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import watcher, { SubscribeCallback } from "@parcel/watcher";
|
|
2
|
+
import { Modification } from "./types";
|
|
3
|
+
import { Emitter } from "./events";
|
|
4
|
+
|
|
5
|
+
export type WatcherEvents = {
|
|
6
|
+
"watcher:file-changed": {
|
|
7
|
+
filePath: string;
|
|
8
|
+
modification: Modification;
|
|
9
|
+
};
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
type SyncFn = (modification: Modification, path: string) => Promise<boolean>;
|
|
13
|
+
type BuildFn = () => Promise<void>;
|
|
14
|
+
|
|
15
|
+
export async function createWatcher(
|
|
16
|
+
emitter: Emitter,
|
|
17
|
+
paths: Array<string>,
|
|
18
|
+
sync: SyncFn,
|
|
19
|
+
build: BuildFn
|
|
20
|
+
) {
|
|
21
|
+
const onChange: SubscribeCallback = async (error, events) => {
|
|
22
|
+
if (error) {
|
|
23
|
+
console.error(error);
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
let rebuild = false;
|
|
28
|
+
|
|
29
|
+
for (const event of events) {
|
|
30
|
+
if (await sync(event.type, event.path)) {
|
|
31
|
+
emitter.emit("watcher:file-changed", {
|
|
32
|
+
filePath: event.path,
|
|
33
|
+
modification: event.type,
|
|
34
|
+
});
|
|
35
|
+
rebuild = true;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (rebuild) {
|
|
40
|
+
await build();
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const subscriptions = await Promise.all(
|
|
45
|
+
paths.map((path) => watcher.subscribe(path, onChange))
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
unsubscribe: async () => {
|
|
50
|
+
await Promise.all(
|
|
51
|
+
subscriptions.map((subscription) => subscription.unsubscribe())
|
|
52
|
+
);
|
|
53
|
+
return;
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import { Writer, createWriter } from "./writer";
|
|
3
|
+
import fs from "node:fs/promises";
|
|
4
|
+
import { existsSync } from "node:fs";
|
|
5
|
+
|
|
6
|
+
describe("writer", () => {
|
|
7
|
+
let writer: Writer;
|
|
8
|
+
|
|
9
|
+
beforeEach(async () => {
|
|
10
|
+
if (existsSync("tmp")) {
|
|
11
|
+
await fs.rm("tmp", { recursive: true });
|
|
12
|
+
}
|
|
13
|
+
writer = await createWriter("tmp");
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
afterEach(async () => {
|
|
17
|
+
if (existsSync("tmp")) {
|
|
18
|
+
await fs.rm("tmp", { recursive: true });
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
async function readJson(path: string) {
|
|
23
|
+
const content = await fs.readFile(path, "utf-8");
|
|
24
|
+
return JSON.parse(content);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
it("should write data files", async () => {
|
|
28
|
+
const collections = [
|
|
29
|
+
{
|
|
30
|
+
name: "test",
|
|
31
|
+
documents: [
|
|
32
|
+
{
|
|
33
|
+
document: "one",
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
document: "two",
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
document: "three",
|
|
40
|
+
},
|
|
41
|
+
],
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
name: "sample",
|
|
45
|
+
documents: [
|
|
46
|
+
{
|
|
47
|
+
document: "four",
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
document: "five",
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
document: "six",
|
|
54
|
+
},
|
|
55
|
+
],
|
|
56
|
+
},
|
|
57
|
+
];
|
|
58
|
+
|
|
59
|
+
await writer.createDataFiles(collections);
|
|
60
|
+
|
|
61
|
+
const allTests = await readJson("tmp/allTests.json");
|
|
62
|
+
expect(allTests).toEqual(["one", "two", "three"]);
|
|
63
|
+
|
|
64
|
+
const allSamples = await readJson("tmp/allSamples.json");
|
|
65
|
+
expect(allSamples).toEqual(["four", "five", "six"]);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("should write javascript file", async () => {
|
|
69
|
+
await fs.writeFile(
|
|
70
|
+
"tmp/allTests.json",
|
|
71
|
+
JSON.stringify(["one", "two", "three"])
|
|
72
|
+
);
|
|
73
|
+
await fs.writeFile(
|
|
74
|
+
"tmp/allSamples.json",
|
|
75
|
+
JSON.stringify(["four", "five", "six"])
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
const collections = [
|
|
79
|
+
{
|
|
80
|
+
name: "test",
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
name: "sample",
|
|
84
|
+
},
|
|
85
|
+
];
|
|
86
|
+
|
|
87
|
+
await writer.createJavaScriptFile({ collections });
|
|
88
|
+
|
|
89
|
+
// @ts-ignore the file is generated before
|
|
90
|
+
const indexJs = await import("./tmp/index.js");
|
|
91
|
+
|
|
92
|
+
expect(indexJs.allTests).toEqual(["one", "two", "three"]);
|
|
93
|
+
expect(indexJs.allSamples).toEqual(["four", "five", "six"]);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("should write type definition file", async () => {
|
|
97
|
+
const collections = [
|
|
98
|
+
{
|
|
99
|
+
name: "test",
|
|
100
|
+
typeName: "Test",
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
name: "sample",
|
|
104
|
+
typeName: "Sample",
|
|
105
|
+
},
|
|
106
|
+
];
|
|
107
|
+
|
|
108
|
+
await writer.createTypeDefinitionFile({
|
|
109
|
+
collections,
|
|
110
|
+
path: "./tmp/config.ts",
|
|
111
|
+
generateTypes: true,
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
const content = await fs.readFile("tmp/index.d.ts", "utf-8");
|
|
115
|
+
expect(content).toContain("export type Test =");
|
|
116
|
+
expect(content).toContain("export type Sample =");
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("should not write type definition file", async () => {
|
|
120
|
+
const collections = [
|
|
121
|
+
{
|
|
122
|
+
name: "test",
|
|
123
|
+
typeName: "Test",
|
|
124
|
+
}
|
|
125
|
+
];
|
|
126
|
+
|
|
127
|
+
await writer.createTypeDefinitionFile({
|
|
128
|
+
collections,
|
|
129
|
+
path: "./tmp/config.ts",
|
|
130
|
+
generateTypes: false,
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
expect(existsSync("tmp/index.d.ts")).toBe(false);
|
|
134
|
+
});
|
|
135
|
+
});
|