@sanoma-ai/workflows 0.9.1 → 0.10.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/README.md +167 -50
- package/dist/apply-steps.js +5 -2
- package/dist/apply-steps.js.map +1 -1
- package/dist/apply.d.ts +3 -0
- package/dist/apply.js +27 -13
- package/dist/apply.js.map +1 -1
- package/dist/approvals.d.ts +1 -0
- package/dist/approvals.js +6 -1
- package/dist/approvals.js.map +1 -1
- package/dist/apps.d.ts +7 -24
- package/dist/apps.js +51 -67
- package/dist/apps.js.map +1 -1
- package/dist/ast.d.ts +9 -7
- package/dist/ast.js +2 -0
- package/dist/ast.js.map +1 -1
- package/dist/authentication.d.ts +24 -0
- package/dist/authentication.js +68 -0
- package/dist/authentication.js.map +1 -0
- package/dist/call-workflow.d.ts +5 -0
- package/dist/call-workflow.js +4 -3
- package/dist/call-workflow.js.map +1 -1
- package/dist/call.d.ts +3 -2
- package/dist/call.js +34 -7
- package/dist/call.js.map +1 -1
- package/dist/cedar.js +14 -1
- package/dist/cedar.js.map +1 -1
- package/dist/change-steps.js +7 -5
- package/dist/change-steps.js.map +1 -1
- package/dist/change.d.ts +7 -0
- package/dist/change.js +7 -0
- package/dist/change.js.map +1 -1
- package/dist/client.d.ts +10 -6
- package/dist/client.js +96 -43
- package/dist/client.js.map +1 -1
- package/dist/collection.d.ts +7 -0
- package/dist/collection.js +5 -2
- package/dist/collection.js.map +1 -1
- package/dist/config.d.ts +9 -0
- package/dist/config.js +45 -9
- package/dist/config.js.map +1 -1
- package/dist/connection.d.ts +168 -0
- package/dist/connection.js +327 -0
- package/dist/connection.js.map +1 -0
- package/dist/credentials.d.ts +7 -53
- package/dist/credentials.js +84 -247
- package/dist/credentials.js.map +1 -1
- package/dist/db.js +21 -2
- package/dist/db.js.map +1 -1
- package/dist/define.d.ts +9 -2
- package/dist/define.js.map +1 -1
- package/dist/definitions.d.ts +20 -0
- package/dist/definitions.js +16 -0
- package/dist/definitions.js.map +1 -0
- package/dist/deployment-key.d.ts +2 -0
- package/dist/deployment-key.js +22 -0
- package/dist/deployment-key.js.map +1 -0
- package/dist/describe.d.ts +9 -177
- package/dist/describe.js +71 -34
- package/dist/describe.js.map +1 -1
- package/dist/description.d.ts +532 -0
- package/dist/description.js +381 -0
- package/dist/description.js.map +1 -0
- package/dist/direct.js +3 -0
- package/dist/direct.js.map +1 -1
- package/dist/drift.d.ts +1 -1
- package/dist/drift.js +1 -1
- package/dist/drift.js.map +1 -1
- package/dist/env.d.ts +2 -0
- package/dist/env.js +2 -2
- package/dist/env.js.map +1 -1
- package/dist/index.d.ts +10 -2
- package/dist/index.js +10 -2
- package/dist/index.js.map +1 -1
- package/dist/ledger.d.ts +2 -0
- package/dist/ledger.js +1 -1
- package/dist/ledger.js.map +1 -1
- package/dist/lint.d.ts +0 -9
- package/dist/lint.js +1 -16
- package/dist/lint.js.map +1 -1
- package/dist/load.d.ts +74 -0
- package/dist/load.js +357 -0
- package/dist/load.js.map +1 -0
- package/dist/lockfile.d.ts +2 -0
- package/dist/lockfile.js +8 -3
- package/dist/lockfile.js.map +1 -1
- package/dist/op.d.ts +10 -1
- package/dist/op.js +1 -1
- package/dist/op.js.map +1 -1
- package/dist/outline.js +13 -4
- package/dist/outline.js.map +1 -1
- package/dist/paths.d.ts +18 -0
- package/dist/paths.js +21 -3
- package/dist/paths.js.map +1 -1
- package/dist/plugin.d.ts +0 -11
- package/dist/plugin.js +3 -18
- package/dist/plugin.js.map +1 -1
- package/dist/policy.d.ts +2 -0
- package/dist/policy.js.map +1 -1
- package/dist/resource.d.ts +10 -4
- package/dist/resource.js +11 -4
- package/dist/resource.js.map +1 -1
- package/dist/resources.d.ts +33 -76
- package/dist/resources.js +219 -227
- package/dist/resources.js.map +1 -1
- package/dist/run.d.ts +2 -0
- package/dist/run.js.map +1 -1
- package/dist/sandbox.d.ts +1 -1
- package/dist/sandbox.js +5 -2
- package/dist/sandbox.js.map +1 -1
- package/dist/scenarios.js +7 -1
- package/dist/scenarios.js.map +1 -1
- package/dist/schema.js +8 -0
- package/dist/schema.js.map +1 -1
- package/dist/secret-store-check.d.ts +11 -0
- package/dist/secret-store-check.js +76 -0
- package/dist/secret-store-check.js.map +1 -0
- package/dist/secret-store.d.ts +106 -0
- package/dist/secret-store.js +228 -0
- package/dist/secret-store.js.map +1 -0
- package/dist/shared.d.ts +17 -11
- package/dist/shared.js +34 -7
- package/dist/shared.js.map +1 -1
- package/dist/sites.d.ts +4 -93
- package/dist/sites.js +61 -84
- package/dist/sites.js.map +1 -1
- package/dist/version.d.ts +2 -1
- package/dist/version.js +2 -1
- package/dist/version.js.map +1 -1
- package/dist/wakeup-driver.js +15 -6
- package/dist/wakeup-driver.js.map +1 -1
- package/dist/worker.js +12 -8
- package/dist/worker.js.map +1 -1
- package/dist/writer.d.ts +53 -14
- package/dist/writer.js +113 -43
- package/dist/writer.js.map +1 -1
- package/oxlint.json +0 -5
- package/package.json +5 -1
- package/src/apply-steps.ts +5 -2
- package/src/apply.ts +54 -21
- package/src/approvals.ts +7 -1
- package/src/apps.ts +54 -85
- package/src/ast.ts +6 -6
- package/src/authentication.ts +74 -0
- package/src/call-workflow.ts +5 -4
- package/src/call.ts +53 -9
- package/src/cedar.ts +14 -1
- package/src/change-steps.ts +7 -4
- package/src/change.ts +10 -0
- package/src/client.ts +108 -41
- package/src/collection.ts +10 -2
- package/src/config.ts +62 -8
- package/src/connection.ts +446 -0
- package/src/credentials.ts +94 -281
- package/src/db.ts +21 -2
- package/src/define.ts +7 -2
- package/src/definitions.ts +31 -0
- package/src/deployment-key.ts +23 -0
- package/src/describe.ts +102 -230
- package/src/description.ts +426 -0
- package/src/direct.ts +3 -0
- package/src/drift.ts +10 -2
- package/src/env.ts +4 -2
- package/src/index.ts +42 -1
- package/src/ledger.ts +3 -1
- package/src/lint.ts +1 -16
- package/src/load.ts +437 -0
- package/src/lockfile.ts +10 -3
- package/src/op.ts +10 -1
- package/src/outline.ts +18 -5
- package/src/paths.ts +24 -2
- package/src/plugin.ts +3 -24
- package/src/policy.ts +2 -0
- package/src/resource.ts +24 -6
- package/src/resources.ts +246 -301
- package/src/run.ts +2 -0
- package/src/sandbox.ts +6 -2
- package/src/scenarios.ts +7 -1
- package/src/schema.ts +10 -0
- package/src/secret-store-check.ts +93 -0
- package/src/secret-store.ts +314 -0
- package/src/shared.ts +35 -10
- package/src/sites.ts +70 -180
- package/src/version.ts +2 -1
- package/src/wakeup-driver.ts +22 -6
- package/src/worker.ts +12 -8
- package/src/writer.ts +145 -56
- package/dist/datafile.d.ts +0 -97
- package/dist/datafile.js +0 -479
- package/dist/datafile.js.map +0 -1
- package/src/datafile.ts +0 -601
package/src/resources.ts
CHANGED
|
@@ -1,92 +1,32 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
type DataFileExport,
|
|
7
|
-
type DataValue,
|
|
8
|
-
readDataFile,
|
|
9
|
-
Reference,
|
|
10
|
-
SECRET_ENTRIES,
|
|
11
|
-
} from "./datafile.ts";
|
|
1
|
+
import type { DeclaredCollection, DeclaredResource, ResourceProblem } from "./description.ts";
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import { mayDeclarePeople } from "./change.ts";
|
|
4
|
+
import { isCollection } from "./collection.ts";
|
|
5
|
+
import { filesRead, type Loaded, loadModules, madeOf, madeWith } from "./load.ts";
|
|
12
6
|
import { type Binding, type Bindings, LOCKFILE, readLockfile } from "./lockfile.ts";
|
|
13
|
-
import {
|
|
14
|
-
import { APPS_DIR, filesUnder, kindDirs } from "./paths.ts";
|
|
7
|
+
import type { Connector } from "./op.ts";
|
|
8
|
+
import { APPS_DIR, COLLECTIONS_FILE, filesUnder, kindDirs, relPath } from "./paths.ts";
|
|
15
9
|
import { peopleConnector, sanoma } from "./person.ts";
|
|
16
10
|
import { wakeupOps } from "./wakeup-type.ts";
|
|
17
|
-
import type
|
|
18
|
-
import { type Declared, type Resource, resourceTypesOf } from "./resource.ts";
|
|
11
|
+
import { DECLARED, type Declared, isDeclared, isObject, type Resource, resourceTypesOf } from "./resource.ts";
|
|
19
12
|
import { Principal } from "./define.ts";
|
|
20
13
|
import { NotAdmittedError } from "./errors.ts";
|
|
21
14
|
import {
|
|
22
15
|
AGENTS,
|
|
23
16
|
ANONYMOUS,
|
|
24
17
|
ANONYMOUS_PRINCIPAL,
|
|
18
|
+
type At,
|
|
25
19
|
declaresPeople,
|
|
26
20
|
isVisitor,
|
|
27
21
|
type PeopleResource,
|
|
28
22
|
personOf,
|
|
29
23
|
principalOf,
|
|
30
24
|
problemLine,
|
|
31
|
-
qualified,
|
|
32
25
|
RUNTIME_AGENTS,
|
|
33
26
|
VISITORS,
|
|
34
27
|
} from "./shared.ts";
|
|
35
28
|
|
|
36
|
-
|
|
37
|
-
export interface DeclaredResource {
|
|
38
|
-
/** `<file>#<export>`, the file relative to the config's root, with `/`: `resources/identity/github.ts#website`. */
|
|
39
|
-
id: string;
|
|
40
|
-
/** The data file it is declared in, as `id` names it: `resources/identity/github.ts`. */
|
|
41
|
-
file: string;
|
|
42
|
-
vendor: string;
|
|
43
|
-
type: string;
|
|
44
|
-
/** What people call it: its identity, else its export's name. */
|
|
45
|
-
name: string;
|
|
46
|
-
/** The import id that finds it unbound: what its type's `find` makes of its fields; absent when the type has none. */
|
|
47
|
-
identity?: string;
|
|
48
|
-
/** The vendor's id of the object the lockfile binds it to (`LOCKFILE`); absent when unbound. */
|
|
49
|
-
binding?: string;
|
|
50
|
-
/** Its `export const` statement, as UTF-16 offsets into the file's text. */
|
|
51
|
-
span: Span;
|
|
52
|
-
/** Its fields, as its type checked them: each resource it names as that resource's `name`. */
|
|
53
|
-
desired: Record<string, unknown>;
|
|
54
|
-
/**
|
|
55
|
-
* Where it names another declared resource: the field's dotted path (a list item's with its
|
|
56
|
-
* index, `teams.0`) to that resource's id.
|
|
57
|
-
*/
|
|
58
|
-
refs: Record<string, string>;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* A collection `resources/data.ts` declares, as read without running it: where it is, for the
|
|
63
|
-
* Resources page. Its fields are the collection's own (`describeConfig`'s `collections`), from
|
|
64
|
-
* the same file, which the runtime imports.
|
|
65
|
-
*/
|
|
66
|
-
export interface DeclaredCollection {
|
|
67
|
-
/** `<file>#<export>`, as a resource's: `resources/data.ts#outbound`. */
|
|
68
|
-
id: string;
|
|
69
|
-
/** `resources/data.ts`, or an app's `apps/<app>/resources/data.ts`. */
|
|
70
|
-
file: string;
|
|
71
|
-
/** The collection's name, its operations' middle segments (`data.<name>.get`): an app's with its app, `crm.contacts`. */
|
|
72
|
-
name: string;
|
|
73
|
-
/** Its `export const` statement, as UTF-16 offsets into the file's text. */
|
|
74
|
-
span: Span;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/** Something wrong in the data files, or in finding them. */
|
|
78
|
-
export interface ResourceProblem {
|
|
79
|
-
/**
|
|
80
|
-
* The data file, relative to the config's root, with `/`: `resources/identity/github.ts`.
|
|
81
|
-
* Absent, with `line` and `column`, when the problem is the config's: it has no root.
|
|
82
|
-
*/
|
|
83
|
-
file?: string;
|
|
84
|
-
/** From 1. */
|
|
85
|
-
line?: number;
|
|
86
|
-
/** From 1, in UTF-16 code units. */
|
|
87
|
-
column?: number;
|
|
88
|
-
message: string;
|
|
89
|
-
}
|
|
29
|
+
export type { DeclaredCollection, DeclaredResource, ResourceProblem };
|
|
90
30
|
|
|
91
31
|
/** What the data files declare: each resource read without problems, the lockfile's bindings, and the problems. */
|
|
92
32
|
export interface DataFiles {
|
|
@@ -98,7 +38,7 @@ export interface DataFiles {
|
|
|
98
38
|
orphans: (Binding & { key: string })[];
|
|
99
39
|
/** The collections `resources/data.ts` declares, in file order. */
|
|
100
40
|
collections: DeclaredCollection[];
|
|
101
|
-
/** By file, line and column. */
|
|
41
|
+
/** By file, then line and column. */
|
|
102
42
|
problems: ResourceProblem[];
|
|
103
43
|
}
|
|
104
44
|
|
|
@@ -113,21 +53,8 @@ export interface DeclaredPeople {
|
|
|
113
53
|
problems: ResourceProblem[];
|
|
114
54
|
}
|
|
115
55
|
|
|
116
|
-
/**
|
|
117
|
-
|
|
118
|
-
/** The last read of the people under each root, by what its data files were then (`filesStamp`). */
|
|
119
|
-
const peopleRead = new Map<string, { stamp: string; read: DeclaredPeople }>();
|
|
120
|
-
|
|
121
|
-
/** Each data file under `root` with its size and modification time: unchanged while the files are. */
|
|
122
|
-
const filesStamp = (root: string) =>
|
|
123
|
-
kindDirs(root, "resources")
|
|
124
|
-
.flatMap(dataFilesUnder)
|
|
125
|
-
.map((path) => {
|
|
126
|
-
// Gone since it was listed (an editor's save by rename): stamped as such, and read next time.
|
|
127
|
-
const stat = statSync(path, { bigint: true, throwIfNoEntry: false });
|
|
128
|
-
return `${path} ${stat?.size} ${stat?.mtimeNs}`;
|
|
129
|
-
})
|
|
130
|
-
.join("\n");
|
|
56
|
+
/** The people each load of the people's files declares: the same answer while none of them changed. */
|
|
57
|
+
const peopleRead = new WeakMap<Map<string, Loaded>, DeclaredPeople>();
|
|
131
58
|
|
|
132
59
|
/** The names no data file may give a group: what each is already. */
|
|
133
60
|
const RESERVED_GROUPS: ReadonlyMap<string, string> = new Map([
|
|
@@ -143,26 +70,39 @@ const RESERVED_GROUPS: ReadonlyMap<string, string> = new Map([
|
|
|
143
70
|
export const inAppPeople = (r: DeclaredResource): boolean =>
|
|
144
71
|
r.vendor === "sanoma" && Object.hasOwn(sanoma, r.type) && r.file.startsWith(`${APPS_DIR}/`);
|
|
145
72
|
|
|
73
|
+
const PEOPLE_CONNECTORS = [peopleConnector, wakeupOps];
|
|
74
|
+
const PEOPLE_TYPES = resourceTypesOf(PEOPLE_CONNECTORS);
|
|
75
|
+
const peopleLoad = (root: string) =>
|
|
76
|
+
loadModules(root, dataFilesOf(root), PEOPLE_CONNECTORS, { only: mayDeclarePeople, types: PEOPLE_TYPES });
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* The company files the people under `root` are read from (`readPeople`), by path relative to the
|
|
80
|
+
* root: each file that may declare them and every company file it imports, so an edit to a helper
|
|
81
|
+
* a people file reads its emails from is a change to the rules too.
|
|
82
|
+
*/
|
|
83
|
+
export const peopleSources = (root: string): ReadonlySet<string> => filesRead(peopleLoad(root));
|
|
84
|
+
|
|
146
85
|
/**
|
|
147
86
|
* The people, agents and groups the data files under `root` declare (`sanoma.person`, `.agent`,
|
|
148
|
-
* `.group`),
|
|
149
|
-
*
|
|
150
|
-
*
|
|
87
|
+
* `.group`), as `principalOf` takes them, with what is wrong in their files; none without a root.
|
|
88
|
+
* Only the files that may declare them (`mayDeclarePeople`) are loaded, with the people's and the
|
|
89
|
+
* wakeup's constructors, which the same `sanoma` declares; loaded again only when one of them
|
|
90
|
+
* changed (by its size and time), so a new colleague is admitted once their file is saved, and a
|
|
91
|
+
* request costs a `stat` per file.
|
|
151
92
|
*/
|
|
152
93
|
export function readPeople(root: string | undefined): DeclaredPeople {
|
|
153
94
|
if (root === undefined) return { people: [], problems: [] };
|
|
154
|
-
const
|
|
155
|
-
const cached = peopleRead.get(
|
|
156
|
-
if (cached
|
|
157
|
-
//
|
|
158
|
-
const read =
|
|
95
|
+
const loaded = peopleLoad(root);
|
|
96
|
+
const cached = peopleRead.get(loaded);
|
|
97
|
+
if (cached) return cached;
|
|
98
|
+
// The people are never bound, so the lockfile is not read.
|
|
99
|
+
const read = declarationsOf(root, loaded, PEOPLE_TYPES, { bindings: {}, problems: [] }, true);
|
|
159
100
|
const ofPeople = read.resources.filter((r) => Object.hasOwn(sanoma, r.type));
|
|
160
101
|
const people = ofPeople.filter((r) => !inAppPeople(r));
|
|
161
|
-
|
|
162
|
-
const own = read.problems.filter((p) => p.file !== LOCKFILE);
|
|
102
|
+
const own = read.problems;
|
|
163
103
|
for (const r of ofPeople.filter(inAppPeople)) {
|
|
164
104
|
own.push({
|
|
165
|
-
|
|
105
|
+
...r.at,
|
|
166
106
|
message: `${r.id} declares a ${r.type} in an app: the people are the company's, declared in resources/ at the root`,
|
|
167
107
|
});
|
|
168
108
|
}
|
|
@@ -170,15 +110,14 @@ export function readPeople(root: string | undefined): DeclaredPeople {
|
|
|
170
110
|
for (const r of people) {
|
|
171
111
|
if (r.type === "group") {
|
|
172
112
|
const reserved = RESERVED_GROUPS.get(r.name);
|
|
173
|
-
if (reserved)
|
|
174
|
-
own.push({ file: r.file, message: `${r.id} is named "${r.name}", ${reserved}: give it another name` });
|
|
113
|
+
if (reserved) own.push({ ...r.at, message: `${r.id} is named "${r.name}", ${reserved}: give it another name` });
|
|
175
114
|
continue;
|
|
176
115
|
}
|
|
177
116
|
const id = r.name;
|
|
178
117
|
const other = ids.get(id);
|
|
179
118
|
if (other || RUNTIME_AGENTS.includes(id) || isVisitor({ id })) {
|
|
180
119
|
own.push({
|
|
181
|
-
|
|
120
|
+
...r.at,
|
|
182
121
|
message: other
|
|
183
122
|
? `${r.id} has the id "${id}", which ${other.id} has: give each person and agent their own`
|
|
184
123
|
: isVisitor({ id })
|
|
@@ -189,7 +128,7 @@ export function readPeople(root: string | undefined): DeclaredPeople {
|
|
|
189
128
|
ids.set(id, r);
|
|
190
129
|
}
|
|
191
130
|
const declared = { people, problems: own };
|
|
192
|
-
peopleRead.set(
|
|
131
|
+
peopleRead.set(loaded, declared);
|
|
193
132
|
return declared;
|
|
194
133
|
}
|
|
195
134
|
|
|
@@ -230,263 +169,227 @@ export function admit(
|
|
|
230
169
|
throw new NotAdmittedError(who);
|
|
231
170
|
}
|
|
232
171
|
|
|
233
|
-
interface FileEntry {
|
|
234
|
-
/** Relative to the config's root, with `/`. */
|
|
235
|
-
rel: string;
|
|
236
|
-
data: DataFile;
|
|
237
|
-
/** Records a problem at an offset into the file. */
|
|
238
|
-
problem: (offset: number, message: string) => void;
|
|
239
|
-
}
|
|
240
|
-
|
|
241
172
|
/**
|
|
242
|
-
*
|
|
243
|
-
* `*.test.ts`, `*.d.ts` and `node_modules
|
|
244
|
-
*
|
|
245
|
-
*
|
|
246
|
-
* its
|
|
173
|
+
* Loads `resources/` under `root`, the config's directory, and each app's: each `.ts` file in it
|
|
174
|
+
* but `*.test.ts`, `*.d.ts` and `node_modules` (`loadModules`), its constructor imports (a
|
|
175
|
+
* connector's `<info.package>/resources`, `sanoma/resources`) served the constructors of
|
|
176
|
+
* `types`. Each export whose value a constructor made is a declaration, `<file>#<export>`, in the
|
|
177
|
+
* file that made it (its non-default name when the file gives two); each resource it names is the
|
|
178
|
+
* declaration that holds that very object.
|
|
247
179
|
*
|
|
248
|
-
* Problems:
|
|
249
|
-
*
|
|
250
|
-
*
|
|
180
|
+
* Problems: a file that throws as it loads (a value its type refuses, a type its vendor does not
|
|
181
|
+
* declare, an import of what the config does not serve), at the place its error's stack gives; a
|
|
182
|
+
* resource made but not exported, or exported under two names; one naming a resource no data file
|
|
183
|
+
* exports; a type that is not the config's; and two resources of one type with one identity (and
|
|
184
|
+
* connection). A declaration with a problem is left out, and so is any that names one.
|
|
251
185
|
*
|
|
252
|
-
* The collections `resources/data.ts` declares are listed
|
|
253
|
-
*
|
|
186
|
+
* The collections `resources/data.ts` declares (`collection()`) are listed by their exports
|
|
187
|
+
* (`collections`), matched to no connector, resource type or lockfile: the runtime imports that
|
|
188
|
+
* file. A collection in another file, or anything else exported from `data.ts`, is a problem.
|
|
254
189
|
*
|
|
255
190
|
* Each declaration is joined to its binding in the lockfile (`readLockfile`) by its key,
|
|
256
|
-
* `<file>#<export>`; a binding no export declares is an orphan
|
|
257
|
-
* binding of another type than its declaration's,
|
|
258
|
-
*
|
|
259
|
-
* `partial` reads part of the files: only those whose source it passes, and in them, an import of
|
|
260
|
-
* a connector not in `connectors`, or of a file left out, is left unread rather than reported.
|
|
191
|
+
* `<file>#<export>`; a binding no export declares is an orphan, unless its file failed to load.
|
|
192
|
+
* Problems: the lockfile's, a binding of another type than its declaration's, two declarations
|
|
193
|
+
* bound to one object, and several wakeups.
|
|
261
194
|
*/
|
|
262
|
-
export function
|
|
195
|
+
export function loadDataFiles(
|
|
263
196
|
root: string | undefined,
|
|
264
197
|
connectors: readonly Connector<any, any>[],
|
|
265
198
|
types: ReadonlyMap<string, Resource> = resourceTypesOf(connectors),
|
|
266
|
-
partial?: (source: string) => boolean,
|
|
267
199
|
): DataFiles {
|
|
268
|
-
const problems: ResourceProblem[] = [];
|
|
269
200
|
if (root === undefined) {
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
});
|
|
274
|
-
return { resources: [], bindings: {}, orphans: [], collections: [], problems };
|
|
201
|
+
const message =
|
|
202
|
+
"The config has no root, so its data files cannot be found: make it with defineConfig, which records its file, or set its `root`";
|
|
203
|
+
return { resources: [], bindings: {}, orphans: [], collections: [], problems: [{ message }] };
|
|
275
204
|
}
|
|
276
|
-
const
|
|
277
|
-
|
|
205
|
+
const loaded = loadModules(root, dataFilesOf(root), connectors, { types });
|
|
206
|
+
return declarationsOf(root, loaded, types, readLockfile(root), false);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/** What the loaded data files declare, joined to the lockfile's bindings. */
|
|
210
|
+
function declarationsOf(
|
|
211
|
+
root: string,
|
|
212
|
+
loaded: Map<string, Loaded>,
|
|
213
|
+
types: ReadonlyMap<string, Resource>,
|
|
214
|
+
lock: { bindings: Bindings; problems: ResourceProblem[] },
|
|
215
|
+
partial: boolean,
|
|
216
|
+
): DataFiles {
|
|
217
|
+
const problems: ResourceProblem[] = [...lock.problems];
|
|
218
|
+
const failed = new Set<string>();
|
|
219
|
+
const collectionsFiles = new Set(
|
|
220
|
+
kindDirs(root, "resources").map((dir) => relPath(root, `${dir}/${COLLECTIONS_FILE}`)),
|
|
221
|
+
);
|
|
222
|
+
const COLLECTIONS_ONLY =
|
|
223
|
+
"resources/data.ts holds collections only, since workflows and the runtime import it: declare a connector's resources in another data file";
|
|
278
224
|
|
|
279
|
-
|
|
280
|
-
const
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
left.add(path);
|
|
225
|
+
// Each exported value, by identity, to the names it is exported under.
|
|
226
|
+
const names = new Map<Declared, At[]>();
|
|
227
|
+
const exported = new Set<Declared>();
|
|
228
|
+
const collections: DeclaredCollection[] = [];
|
|
229
|
+
for (const [file, l] of loaded) {
|
|
230
|
+
if ("failedIn" in l) {
|
|
231
|
+
failed.add(file);
|
|
287
232
|
continue;
|
|
288
233
|
}
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
if (entries.get("sanoma/resources") === "sanoma") for (const entry of SECRET_ENTRIES) entries.set(entry, "sanoma");
|
|
305
|
-
|
|
306
|
-
// What each file's names stand for, and every resource read by its id.
|
|
307
|
-
const vendors = new Map<FileEntry, Map<string, string>>();
|
|
308
|
-
const named = new Map<FileEntry, Map<string, string>>();
|
|
309
|
-
const declarations = new Map<string, { file: FileEntry; exp: DataFileExport }>();
|
|
310
|
-
for (const [path, file] of files) {
|
|
311
|
-
const fileVendors = new Map<string, string>();
|
|
312
|
-
const fileNamed = new Map<string, string>();
|
|
313
|
-
for (const imp of file.data.imports) {
|
|
314
|
-
if (imp.kind === "constructors") {
|
|
315
|
-
const vendor = entries.get(imp.source);
|
|
316
|
-
if (vendor === undefined && partial) continue;
|
|
317
|
-
if (vendor === undefined) {
|
|
318
|
-
file.problem(
|
|
319
|
-
imp.start,
|
|
320
|
-
`"${imp.source}" is not the resources entry of a connector in the config's \`connectors\`: add the connector, and import from its package's \`/resources\``,
|
|
321
|
-
);
|
|
322
|
-
} else if (imp.imported !== vendor) {
|
|
323
|
-
file.problem(
|
|
324
|
-
imp.start,
|
|
325
|
-
`"${imp.source}" exports its constructors as ${vendor}: \`import { ${vendor} } from "${imp.source}"\``,
|
|
326
|
-
);
|
|
234
|
+
if ("error" in l) {
|
|
235
|
+
failed.add(file);
|
|
236
|
+
const message = loadProblem(l.error, root, file, partial);
|
|
237
|
+
if (message !== undefined)
|
|
238
|
+
problems.push({ file, ...(l.line !== undefined && { line: l.line, column: l.column ?? 1 }), message });
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
241
|
+
for (const [name, value] of l.exports) {
|
|
242
|
+
const at = { file, export: name };
|
|
243
|
+
if (isCollection(value)) {
|
|
244
|
+
if (!collectionsFiles.has(file)) {
|
|
245
|
+
problems.push({
|
|
246
|
+
...at,
|
|
247
|
+
message: `a collection is declared in resources/data.ts, which workflows and the runtime import: move ${name} there`,
|
|
248
|
+
});
|
|
327
249
|
} else {
|
|
328
|
-
|
|
250
|
+
collections.push({ id: `${file}#${name}`, file, name: value.name, at });
|
|
329
251
|
}
|
|
330
|
-
|
|
252
|
+
} else if (isDeclared(value)) {
|
|
253
|
+
exported.add(value);
|
|
254
|
+
if (collectionsFiles.has(file)) problems.push({ ...at, message: COLLECTIONS_ONLY });
|
|
255
|
+
else names.set(value, [...(names.get(value) ?? []), at]);
|
|
331
256
|
}
|
|
332
|
-
const target = resolve(dirname(path), imp.source);
|
|
333
|
-
const other = files.get(target);
|
|
334
|
-
if (!other && left.has(target)) continue;
|
|
335
|
-
if (!other) {
|
|
336
|
-
file.problem(imp.start, `"${imp.source}" is not a data file under resources/`);
|
|
337
|
-
} else if (!other.data.declared.includes(imp.imported)) {
|
|
338
|
-
file.problem(imp.start, `${imp.imported} is not a resource ${other.rel} declares`);
|
|
339
|
-
} else if (other.data.exports.some((e) => e.name === imp.imported)) {
|
|
340
|
-
fileNamed.set(imp.local, `${other.rel}#${imp.imported}`);
|
|
341
|
-
}
|
|
342
|
-
// Else its value was refused, and reported in its file: a reference to it fails quietly.
|
|
343
257
|
}
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Each declaration: one name per object, a non-default one over `default`.
|
|
261
|
+
const declarations = new Map<string, { value: Declared; at: At }>();
|
|
262
|
+
const idOf = new Map<Declared, string>();
|
|
263
|
+
for (const [value, ats] of names) {
|
|
264
|
+
const named = ats.filter((at) => at.export !== "default");
|
|
265
|
+
const kept = named.length ? named : ats;
|
|
266
|
+
if (kept.length > 1) {
|
|
267
|
+
const [first, ...rest] = kept.map((at) => `${at.file}#${at.export}`);
|
|
268
|
+
problems.push({
|
|
269
|
+
...kept[1]!,
|
|
270
|
+
message: `${rest.join(", ")} and ${first} are one ${value[DECLARED]}: export each resource under one name`,
|
|
271
|
+
});
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
274
|
+
const at = kept[0]!;
|
|
275
|
+
const type = types.get(value[DECLARED]);
|
|
276
|
+
if (type === undefined || madeOf(value) === undefined) {
|
|
277
|
+
if (!partial)
|
|
278
|
+
problems.push({
|
|
279
|
+
...at,
|
|
280
|
+
message: `${value[DECLARED]} is not a resource type of a connector in the config's \`connectors\`: add the connector, and import from its package's \`/resources\``,
|
|
281
|
+
});
|
|
282
|
+
continue;
|
|
348
283
|
}
|
|
349
|
-
|
|
350
|
-
|
|
284
|
+
const id = `${at.file}#${at.export}`;
|
|
285
|
+
declarations.set(id, { value, at });
|
|
286
|
+
idOf.set(value, id);
|
|
287
|
+
}
|
|
288
|
+
// A resource made and never exported is declared nowhere: a typo'd export, or one written inline.
|
|
289
|
+
for (const value of madeWith(loaded)) {
|
|
290
|
+
const made = madeOf(value)!;
|
|
291
|
+
if (!isDeclared(value) || exported.has(value) || failed.has(made.file)) continue;
|
|
292
|
+
problems.push({
|
|
293
|
+
file: made.file,
|
|
294
|
+
message: `${aResource(value)} is made here but not exported: declare each resource as its own \`export const <name> = <vendor>.<type>({ … })\``,
|
|
295
|
+
});
|
|
351
296
|
}
|
|
352
297
|
|
|
353
|
-
// Each
|
|
354
|
-
const done = new Map<string,
|
|
355
|
-
const visiting: string[] = [];
|
|
298
|
+
// Each declaration after the ones it names; one with a problem is left out, and so is any that names it.
|
|
299
|
+
const done = new Map<string, Record<string, string> | undefined>();
|
|
356
300
|
const byName = new Map<string, string>();
|
|
357
|
-
|
|
301
|
+
const leaveOut = (id: string, message?: string) => {
|
|
302
|
+
if (message !== undefined) problems.push({ ...declarations.get(id)!.at, message });
|
|
303
|
+
done.set(id, undefined);
|
|
304
|
+
};
|
|
305
|
+
function declare(id: string): Record<string, string> | undefined {
|
|
358
306
|
if (done.has(id)) return done.get(id);
|
|
359
|
-
const {
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
307
|
+
const { value } = declarations.get(id)!;
|
|
308
|
+
const refs: Record<string, string> = {};
|
|
309
|
+
for (const [path, target] of Object.entries(madeOf(value)!.refers)) {
|
|
310
|
+
const targetId = idOf.get(target);
|
|
311
|
+
// A partial load serves only some types: a resource of another (a wakeup's host) is not its to judge.
|
|
312
|
+
if (targetId === undefined && partial && !types.has(target[DECLARED])) continue;
|
|
313
|
+
if (targetId === undefined) {
|
|
314
|
+
leaveOut(
|
|
315
|
+
id,
|
|
316
|
+
`${path} names ${aResource(target)} that no data file exports: export it from a data file and import it here`,
|
|
317
|
+
);
|
|
318
|
+
return undefined;
|
|
319
|
+
}
|
|
320
|
+
if (!declare(targetId)) return void done.set(id, undefined);
|
|
321
|
+
refs[path] = targetId;
|
|
363
322
|
}
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
visiting.pop();
|
|
367
|
-
if (result?.declared.name !== undefined) {
|
|
368
|
-
const { vendor, type, name } = result.declared;
|
|
369
|
-
const key = `${vendor}.${type} ${JSON.stringify(name)}`;
|
|
323
|
+
if (value.name !== undefined) {
|
|
324
|
+
const key = `${value[DECLARED]} ${JSON.stringify(value.name)}${value.connection ? ` (${value.connection.$connection})` : ""}`;
|
|
370
325
|
const first = byName.get(key);
|
|
371
|
-
if (first
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
exp.start,
|
|
326
|
+
if (first !== undefined) {
|
|
327
|
+
leaveOut(
|
|
328
|
+
id,
|
|
375
329
|
`${key} is declared twice, also as ${first}: declare each resource once, and import it where it is used`,
|
|
376
330
|
);
|
|
377
|
-
done.set(id, undefined);
|
|
378
331
|
return undefined;
|
|
379
332
|
}
|
|
333
|
+
byName.set(key, id);
|
|
380
334
|
}
|
|
381
|
-
done.set(id,
|
|
382
|
-
return
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
function build(file: FileEntry, exp: DataFileExport) {
|
|
386
|
-
// An unknown constructors import was reported with the import.
|
|
387
|
-
const vendor = vendors.get(file)!.get(exp.vendor);
|
|
388
|
-
if (vendor === undefined) return undefined;
|
|
389
|
-
const resource = types.get(`${vendor}.${exp.type}`);
|
|
390
|
-
if (!resource) {
|
|
391
|
-
const known = [...types.values()].filter((r) => r.vendor === vendor).map((r) => r.type);
|
|
392
|
-
file.problem(exp.start, `${vendor} has no resource type ${exp.type}: its types are ${known.join(", ")}`);
|
|
393
|
-
return undefined;
|
|
394
|
-
}
|
|
395
|
-
const refs: Record<string, string> = {};
|
|
396
|
-
let ok = true;
|
|
397
|
-
// The fields with each name of a resource as that resource, and where each is named.
|
|
398
|
-
const given = (v: DataValue, at: string): unknown => {
|
|
399
|
-
if (v instanceof Reference) {
|
|
400
|
-
// A name that did not resolve was reported where it is bound.
|
|
401
|
-
const target = named.get(file)!.get(v.name);
|
|
402
|
-
const declared = target === undefined ? undefined : declare(target)?.declared;
|
|
403
|
-
if (!declared) ok = false;
|
|
404
|
-
else refs[at] = target!;
|
|
405
|
-
return declared;
|
|
406
|
-
}
|
|
407
|
-
if (Array.isArray(v)) return v.map((item, i) => given(item, `${at}.${i}`));
|
|
408
|
-
if (v === null || typeof v !== "object") return v;
|
|
409
|
-
return Object.fromEntries(Object.entries(v).map(([k, item]) => [k, given(item, at ? `${at}.${k}` : k)]));
|
|
410
|
-
};
|
|
411
|
-
const fields = given(exp.desired, "");
|
|
412
|
-
if (!ok) return undefined;
|
|
413
|
-
try {
|
|
414
|
-
// Checked as running the file would: by the resource type, which allows a resource only where its `fields.references` say.
|
|
415
|
-
return { declared: resource(fields as never), refs };
|
|
416
|
-
} catch (e) {
|
|
417
|
-
// One line per problem: zod's own spans lines.
|
|
418
|
-
file.problem(exp.start, `export const ${exp.name}: ${(e as Error).message.replaceAll(/\s*\n\s*/g, " ")}`);
|
|
419
|
-
return undefined;
|
|
420
|
-
}
|
|
335
|
+
done.set(id, refs);
|
|
336
|
+
return refs;
|
|
421
337
|
}
|
|
422
338
|
|
|
423
339
|
// Each binding against its declaration: of its type, and the only one bound to its object.
|
|
424
340
|
const boundBy = new Map<string, string>();
|
|
425
|
-
for (const [id, {
|
|
341
|
+
for (const [id, { value }] of declarations) {
|
|
426
342
|
const binding = lock.bindings[id];
|
|
427
|
-
|
|
428
|
-
const
|
|
429
|
-
if (binding === undefined || vendor === undefined) continue;
|
|
430
|
-
const type = `${vendor}.${exp.type}`;
|
|
431
|
-
const object = `${binding.type} ${binding.id}`;
|
|
343
|
+
if (binding === undefined) continue;
|
|
344
|
+
const object = `${binding.type} ${binding.id}${binding.connection ? ` (${binding.connection.$connection})` : ""}`;
|
|
432
345
|
const first = boundBy.get(object);
|
|
433
346
|
const why =
|
|
434
|
-
binding.
|
|
435
|
-
? `${id}
|
|
436
|
-
:
|
|
437
|
-
? `${id} is bound to
|
|
438
|
-
: undefined
|
|
347
|
+
value.connection !== undefined && binding.connection?.$connection !== value.connection.$connection
|
|
348
|
+
? `${id} changes the connection of a bound resource: explicitly unbind it before selecting another account`
|
|
349
|
+
: binding.type !== value[DECLARED]
|
|
350
|
+
? `${id} is bound to a ${binding.type} in ${LOCKFILE}, but declares a ${value[DECLARED]}`
|
|
351
|
+
: first !== undefined
|
|
352
|
+
? `${id} is bound to the same ${object} as ${first} in ${LOCKFILE}: bind each object once`
|
|
353
|
+
: undefined;
|
|
439
354
|
if (why === undefined) boundBy.set(object, id);
|
|
440
|
-
else
|
|
441
|
-
// Left out, as any declaration with a problem, and so is any that names it.
|
|
442
|
-
file.problem(exp.start, why);
|
|
443
|
-
done.set(id, undefined);
|
|
444
|
-
}
|
|
355
|
+
else leaveOut(id, why);
|
|
445
356
|
}
|
|
446
357
|
// An instance registers with one wakeup: several are each a problem, and left out, as is any that names one.
|
|
447
|
-
const wakeups = [...declarations].filter(
|
|
448
|
-
([, { file, exp }]) => types.get(`${vendors.get(file)!.get(exp.vendor)}.${exp.type}`)?.wakeup,
|
|
449
|
-
);
|
|
358
|
+
const wakeups = [...declarations.keys()].filter((id) => types.get(declarations.get(id)!.value[DECLARED])?.wakeup);
|
|
450
359
|
if (wakeups.length > 1) {
|
|
451
|
-
for (const
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
360
|
+
for (const id of wakeups) {
|
|
361
|
+
leaveOut(
|
|
362
|
+
id,
|
|
363
|
+
`an instance has one wakeup: leave out this one or ${wakeups.filter((other) => other !== id).join(", ")}`,
|
|
364
|
+
);
|
|
455
365
|
}
|
|
456
366
|
}
|
|
457
|
-
|
|
367
|
+
// A binding under a file that failed to load is no orphan: what it declares is not known.
|
|
458
368
|
const orphans = Object.entries(lock.bindings)
|
|
459
|
-
.filter(([key]) => !
|
|
369
|
+
.filter(([key]) => !declarations.has(key) && !failed.has(key.slice(0, key.lastIndexOf("#"))))
|
|
460
370
|
.map(([key, binding]) => ({ key, ...binding }));
|
|
461
371
|
|
|
462
372
|
const resources: DeclaredResource[] = [];
|
|
463
|
-
for (const [id, {
|
|
464
|
-
const
|
|
465
|
-
if (!
|
|
466
|
-
const { vendor, type, name: identity, desired } =
|
|
373
|
+
for (const [id, { value, at }] of declarations) {
|
|
374
|
+
const refs = declare(id);
|
|
375
|
+
if (!refs) continue;
|
|
376
|
+
const { vendor, type, name: identity, desired, connection } = value;
|
|
467
377
|
const binding = lock.bindings[id]?.id;
|
|
468
378
|
resources.push({
|
|
469
379
|
id,
|
|
470
|
-
file: file
|
|
380
|
+
file: at.file,
|
|
471
381
|
vendor,
|
|
472
382
|
type,
|
|
473
|
-
name: identity ??
|
|
383
|
+
name: identity ?? at.export,
|
|
474
384
|
...(identity !== undefined && { identity }),
|
|
475
385
|
...(binding !== undefined && { binding }),
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
386
|
+
at,
|
|
387
|
+
// A copy: the loaded value is the generation's, read again while no file changes.
|
|
388
|
+
desired: structuredClone(desired) as Record<string, unknown>,
|
|
389
|
+
...(connection && { connection: { ...connection } }),
|
|
390
|
+
refs,
|
|
479
391
|
});
|
|
480
392
|
}
|
|
481
|
-
const collections = [...files.values()].flatMap(({ rel, data }) =>
|
|
482
|
-
data.collections.map((c) => ({
|
|
483
|
-
id: `${rel}#${c.export}`,
|
|
484
|
-
file: rel,
|
|
485
|
-
// An app's, `apps/<app>/resources/data.ts`, is named with its app, as `collection()` names it.
|
|
486
|
-
name: qualified(rel.startsWith(`${APPS_DIR}/`) ? rel.split("/")[1] : undefined, c.name),
|
|
487
|
-
span: [c.start, c.end] as Span,
|
|
488
|
-
})),
|
|
489
|
-
);
|
|
490
393
|
problems.sort(
|
|
491
394
|
(a, b) =>
|
|
492
395
|
(a.file ?? "").localeCompare(b.file ?? "") || (a.line ?? 0) - (b.line ?? 0) || (a.column ?? 0) - (b.column ?? 0),
|
|
@@ -494,6 +397,48 @@ export function readDataFiles(
|
|
|
494
397
|
return { resources, bindings: lock.bindings, orphans, collections, problems };
|
|
495
398
|
}
|
|
496
399
|
|
|
400
|
+
/** A resource as a problem names it: `a github.repository "website"`, or without its identity when it has none. */
|
|
401
|
+
const aResource = (value: Declared) => `a ${value[DECLARED]}${value.name === undefined ? "" : ` "${value.name}"`}`;
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* What a data file that failed to load is told, on one line: an import Node could not find (a
|
|
405
|
+
* connector the config does not have, a file that is not there), top-level `await`, or the
|
|
406
|
+
* error's own message (a constructor's refusal: `github.repository: no field wiki`). Nothing, when
|
|
407
|
+
* `partial`, for a connector's constructors left out of the load in a file that declares no
|
|
408
|
+
* person, agent or group (`sanoma.wakeup` beside its host's connector).
|
|
409
|
+
*/
|
|
410
|
+
function loadProblem(error: unknown, root: string, file: string, partial: boolean): string | undefined {
|
|
411
|
+
const { code, message } = isObject(error) ? (error as { code?: unknown; message?: unknown }) : {};
|
|
412
|
+
const text = typeof message === "string" ? message : String(error);
|
|
413
|
+
if (code === "ERR_REQUIRE_ASYNC_MODULE")
|
|
414
|
+
return "a data file is loaded synchronously: leave out top-level `await`, and compute values where they are used";
|
|
415
|
+
if (code === "ERR_MODULE_NOT_FOUND") {
|
|
416
|
+
const missing = /^Cannot find (?:package|module) '([^']+)'/.exec(text)?.[1];
|
|
417
|
+
const source = readFileSync(`${root}/${file}`, "utf8");
|
|
418
|
+
// The specifier as written: Node names a package by its name, a file by its path.
|
|
419
|
+
const written =
|
|
420
|
+
missing &&
|
|
421
|
+
[...source.matchAll(/from\s*["']([^"']+)["']/g)]
|
|
422
|
+
.map((m) => m[1]!)
|
|
423
|
+
.find(
|
|
424
|
+
(spec) =>
|
|
425
|
+
spec === missing ||
|
|
426
|
+
spec.startsWith(`${missing}/`) ||
|
|
427
|
+
(missing.startsWith("/") && missing.endsWith(`/${spec.replace(/^(?:\.\.?\/)+/, "")}`)),
|
|
428
|
+
);
|
|
429
|
+
if (written?.endsWith("/resources") && partial && !/\.\s*(?:person|agent|group)\s*\(/.test(source))
|
|
430
|
+
return undefined;
|
|
431
|
+
if (written?.endsWith("/resources"))
|
|
432
|
+
return `"${written}" is not the resources entry of a connector in the config's \`connectors\`: add the connector, and import from its package's \`/resources\``;
|
|
433
|
+
if (written !== undefined)
|
|
434
|
+
return `"${written}" cannot be found: import a file that is there, or a package the company has`;
|
|
435
|
+
}
|
|
436
|
+
return text.replaceAll(/\s*\n\s*/g, " ");
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/** The data files under `root`: the root's `resources/`, then each app's, each sorted. */
|
|
440
|
+
const dataFilesOf = (root: string): string[] => kindDirs(root, "resources").flatMap(dataFilesUnder);
|
|
441
|
+
|
|
497
442
|
/** The data files under `dir`, sorted: regular `.ts` files, less tests, declarations and `node_modules`. */
|
|
498
443
|
const dataFilesUnder = (dir: string): string[] =>
|
|
499
444
|
filesUnder(dir, (name) => name.endsWith(".ts") && !name.endsWith(".test.ts") && !name.endsWith(".d.ts"));
|