backeneko 0.0.0-stage → 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/LICENSE +21 -0
- package/README.md +135 -2
- package/package.json +49 -4
- package/src/args.ts +64 -0
- package/src/cli.ts +199 -0
- package/src/collect.ts +38 -0
- package/src/compile.ts +263 -0
- package/src/directives.ts +125 -0
- package/src/generate.ts +395 -0
- package/src/graphql-internal.ts +8 -0
- package/src/merge.ts +125 -0
- package/src/mocks.ts +199 -0
- package/src/resolvers.ts +89 -0
- package/src/runtime.ts +12 -0
- package/src/server.ts +216 -0
- package/src/watch.ts +29 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 backeneko contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,136 @@
|
|
|
1
|
-
#
|
|
1
|
+
# backeneko
|
|
2
|
+
```text
|
|
3
|
+
/\_/\
|
|
4
|
+
( =.= ) ( (
|
|
5
|
+
/ \ / /
|
|
6
|
+
(_|__|_)/_/
|
|
7
|
+
```
|
|
2
8
|
|
|
3
|
-
|
|
9
|
+
|
|
10
|
+
Mock GraphQL backend generated from `.graphql` schema files, with hot reload.
|
|
11
|
+
|
|
12
|
+
## Requirements
|
|
13
|
+
|
|
14
|
+
Bun >= 1.1 — backeneko runs on Bun (`Bun.serve`, `Bun.build`, recursive
|
|
15
|
+
`fs.watch`).
|
|
16
|
+
|
|
17
|
+
## Install
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
bun add -g backeneko # or: npm i -g backeneko
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Quick start
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
backeneko --source=./schemas --port=4000
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
`--source` is scanned recursively for `*.graphql` / `*.gql`. Open
|
|
30
|
+
<http://localhost:4000/> for a minimal query inspector, or POST to `/graphql`:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
curl -s http://localhost:4000/graphql \
|
|
34
|
+
-H 'content-type: application/json' \
|
|
35
|
+
-d '{"query":"{ users { id name role } }"}'
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## How it works
|
|
39
|
+
|
|
40
|
+
On the first run backeneko scans `--source`, merges the files into one schema
|
|
41
|
+
and creates a workspace:
|
|
42
|
+
|
|
43
|
+
```
|
|
44
|
+
.backeneko/
|
|
45
|
+
├── schema.graphql # merged schema (rewritten each start)
|
|
46
|
+
├── tsconfig.json # @<kind>/* aliases: override → generated
|
|
47
|
+
├── generated/ # disposable mirror, rewritten each start
|
|
48
|
+
│ ├── entities/
|
|
49
|
+
│ │ ├── User.ts # mock entity (default export)
|
|
50
|
+
│ │ └── Post.ts
|
|
51
|
+
│ ├── queries/
|
|
52
|
+
│ │ ├── users.ts # one file per Query field: (parent, args, ...) => value
|
|
53
|
+
│ │ └── user.ts
|
|
54
|
+
│ └── mutations/
|
|
55
|
+
│ └── createUser.ts
|
|
56
|
+
├── override/ # your files win; never written by backeneko
|
|
57
|
+
│ └── entities/User.ts # shadows generated/entities/User.ts
|
|
58
|
+
└── cache/ # bundled mock modules (per content hash)
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
- Every root field gets its own TS file exporting a resolver function.
|
|
62
|
+
- Every entity type gets a file exporting a `default` mock value.
|
|
63
|
+
- `generated/` is disposable: with `--source` it is rewritten on every start
|
|
64
|
+
(added/removed fields and types are synced) and files you edited there are
|
|
65
|
+
overwritten.
|
|
66
|
+
- Put lasting changes in `override/` with the same path — an override file
|
|
67
|
+
always shadows the generated one and is never written or removed. For
|
|
68
|
+
example, create `override/entities/User.ts` to replace the generated
|
|
69
|
+
`generated/entities/User.ts`.
|
|
70
|
+
- Relations are imported through `@entities/...`, which resolves override-first,
|
|
71
|
+
so a generated entity picks up your overridden related entity automatically.
|
|
72
|
+
- Edit any mock file — the backend reloads automatically.
|
|
73
|
+
- Entity named exports matching a field name override that field's resolver.
|
|
74
|
+
|
|
75
|
+
Without `--source`, an existing `.backeneko` is served as-is.
|
|
76
|
+
|
|
77
|
+
## Mock directives
|
|
78
|
+
|
|
79
|
+
Pin an entity field's generated value straight from the schema with
|
|
80
|
+
`@backeneko(value: "...")` or its alias `@mock(value: "...")`:
|
|
81
|
+
|
|
82
|
+
```graphql
|
|
83
|
+
type Product {
|
|
84
|
+
id: ID!
|
|
85
|
+
title: String @backeneko(value: "Pinned Title")
|
|
86
|
+
price: Int @mock(value: "199")
|
|
87
|
+
inStock: Boolean @backeneko(value: "true")
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
- No need to declare the directive — backeneko provides it on its own.
|
|
92
|
+
- The value is a string and is cast to the field's type where possible:
|
|
93
|
+
`Int`/`Float` → number, `Boolean` → boolean, `String`/`ID`/enum/custom
|
|
94
|
+
scalars → string. An uncastable value is ignored with a warning and the type
|
|
95
|
+
default is kept.
|
|
96
|
+
- Only single scalar/enum fields are supported. On list or object/interface/
|
|
97
|
+
union fields the directive is ignored with a warning.
|
|
98
|
+
- Generated values are only defaults: an `override/<kind>/<name>.ts` file still
|
|
99
|
+
replaces them entirely.
|
|
100
|
+
|
|
101
|
+
## CLI
|
|
102
|
+
|
|
103
|
+
```
|
|
104
|
+
backeneko --source=<dir> [--port=4000] [--dir=.backeneko] [--no-validate]
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
- `--source=<dir>` — directory with schema files (recursive).
|
|
108
|
+
- `--port=<n>` — HTTP port (default `4000`).
|
|
109
|
+
- `--dir=<dir>` — workspace directory (default `.backeneko`).
|
|
110
|
+
- `--no-validate` — skip validating incoming queries against the schema.
|
|
111
|
+
|
|
112
|
+
## Development
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
bun install
|
|
116
|
+
bun run demo # run against examples/source on :4000
|
|
117
|
+
bun run typecheck
|
|
118
|
+
bun test
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## Publishing
|
|
122
|
+
|
|
123
|
+
Releases are published to npm by the [Publish](.github/workflows/publish.yml)
|
|
124
|
+
GitHub Actions workflow. To cut a release:
|
|
125
|
+
|
|
126
|
+
1. Bump `version` in `package.json` and commit it.
|
|
127
|
+
2. Push a matching tag: `git tag v0.1.0 && git push origin v0.1.0`.
|
|
128
|
+
|
|
129
|
+
The workflow runs typecheck + tests and publishes with provenance. It needs:
|
|
130
|
+
|
|
131
|
+
- an `NPM_TOKEN` repository secret (an npm automation token with publish rights);
|
|
132
|
+
- a `repository` field in `package.json` pointing at the public GitHub repo
|
|
133
|
+
(required by npm provenance) — the `repository` field already points at
|
|
134
|
+
`iliyat/backeneko`.
|
|
135
|
+
|
|
136
|
+
See `AGENTS.md` for contributor guidance.
|
package/package.json
CHANGED
|
@@ -1,6 +1,51 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "backeneko",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Mock GraphQL backend generated from .graphql schema files, with hot reload",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/iliyat/backeneko.git"
|
|
9
|
+
},
|
|
10
|
+
"homepage": "https://github.com/iliyat/backeneko#readme",
|
|
11
|
+
"bugs": {
|
|
12
|
+
"url": "https://github.com/iliyat/backeneko/issues"
|
|
13
|
+
},
|
|
14
|
+
"bin": {
|
|
15
|
+
"backeneko": "./src/cli.ts"
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"src"
|
|
19
|
+
],
|
|
20
|
+
"engines": {
|
|
21
|
+
"bun": ">=1.1.0"
|
|
22
|
+
},
|
|
23
|
+
"packageManager": "bun@1.3.14",
|
|
24
|
+
"keywords": [
|
|
25
|
+
"graphql",
|
|
26
|
+
"mock",
|
|
27
|
+
"schema",
|
|
28
|
+
"bun",
|
|
29
|
+
"backend",
|
|
30
|
+
"hot-reload",
|
|
31
|
+
"sdl"
|
|
32
|
+
],
|
|
33
|
+
"license": "MIT",
|
|
34
|
+
"publishConfig": {
|
|
35
|
+
"access": "public"
|
|
36
|
+
},
|
|
37
|
+
"scripts": {
|
|
38
|
+
"start": "bun run src/cli.ts",
|
|
39
|
+
"demo": "bun run src/cli.ts --source=examples/source --port=4000",
|
|
40
|
+
"typecheck": "tsc --noEmit",
|
|
41
|
+
"test": "bun test",
|
|
42
|
+
"prepublishOnly": "bun run typecheck && bun test"
|
|
43
|
+
},
|
|
44
|
+
"dependencies": {
|
|
45
|
+
"graphql": "^16.11.0"
|
|
46
|
+
},
|
|
47
|
+
"devDependencies": {
|
|
48
|
+
"@types/bun": "^1.3.4",
|
|
49
|
+
"typescript": "^5.9.3"
|
|
50
|
+
}
|
|
51
|
+
}
|
package/src/args.ts
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { parseArgs } from "node:util";
|
|
2
|
+
|
|
3
|
+
export interface CliOptions {
|
|
4
|
+
source: string | null;
|
|
5
|
+
port: number;
|
|
6
|
+
dir: string;
|
|
7
|
+
validate: boolean;
|
|
8
|
+
help: boolean;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const DEFAULT_PORT = 4000;
|
|
12
|
+
export const DEFAULT_WORKSPACE_DIR = ".backeneko";
|
|
13
|
+
|
|
14
|
+
export function parseCliArgs(argv: string[] = process.argv.slice(2)): CliOptions {
|
|
15
|
+
const { values } = parseArgs({
|
|
16
|
+
args: argv,
|
|
17
|
+
options: {
|
|
18
|
+
source: { type: "string" },
|
|
19
|
+
port: { type: "string" },
|
|
20
|
+
dir: { type: "string" },
|
|
21
|
+
"no-validate": { type: "boolean" },
|
|
22
|
+
help: { type: "boolean", short: "h" },
|
|
23
|
+
},
|
|
24
|
+
allowPositionals: true,
|
|
25
|
+
strict: true,
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
const port = values.port === undefined ? DEFAULT_PORT : Number(values.port);
|
|
29
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
30
|
+
throw new Error(`invalid --port value: ${values.port}`);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
source: values.source ?? null,
|
|
35
|
+
port,
|
|
36
|
+
dir: values.dir ?? DEFAULT_WORKSPACE_DIR,
|
|
37
|
+
validate: !(values["no-validate"] ?? false),
|
|
38
|
+
help: values.help ?? false,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function printHelp(): void {
|
|
43
|
+
console.log(`backeneko — mock GraphQL backend generated from .graphql schema files
|
|
44
|
+
|
|
45
|
+
Usage:
|
|
46
|
+
backeneko [--source=<schema-directory>] [options]
|
|
47
|
+
|
|
48
|
+
Options:
|
|
49
|
+
--source=<dir> Directory with .graphql/.gql schema files. Regenerates the
|
|
50
|
+
workspace: overwrites schema.graphql and everything under
|
|
51
|
+
generated/, leaves override/ alone. Omit to serve <dir> as-is.
|
|
52
|
+
--port=<n> HTTP port (default: ${DEFAULT_PORT})
|
|
53
|
+
--dir=<dir> Workspace directory (default: ${DEFAULT_WORKSPACE_DIR})
|
|
54
|
+
--no-validate Skip validating queries against the schema (default: validate)
|
|
55
|
+
-h, --help Show this help
|
|
56
|
+
|
|
57
|
+
Behavior:
|
|
58
|
+
* with --source: schema.graphql and generated/ are rewritten on every start,
|
|
59
|
+
including files whose type or field disappeared from the schema
|
|
60
|
+
* override/entities|queries|mutations|subscriptions/<name>.ts wins over
|
|
61
|
+
generated/<same path>; overrides are loaded, never written or removed
|
|
62
|
+
* without --source: an existing <dir> is served unchanged
|
|
63
|
+
* mock files and schema hot-reload on change`);
|
|
64
|
+
}
|
package/src/cli.ts
ADDED
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { readdir, readFile } from "node:fs/promises";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { parseCliArgs, printHelp } from "./args.ts";
|
|
6
|
+
import { collectSchemaFiles } from "./collect.ts";
|
|
7
|
+
import { compileDocument } from "./compile.ts";
|
|
8
|
+
import {
|
|
9
|
+
GENERATED_DIR,
|
|
10
|
+
OVERRIDE_DIR,
|
|
11
|
+
ensureWorkspaceTsconfig,
|
|
12
|
+
generateWorkspace,
|
|
13
|
+
} from "./generate.ts";
|
|
14
|
+
import { mergeSchemaDocuments, renderSdl } from "./merge.ts";
|
|
15
|
+
import { loadMockModules } from "./mocks.ts";
|
|
16
|
+
import { buildExecutableSchema } from "./resolvers.ts";
|
|
17
|
+
import { runtime } from "./runtime.ts";
|
|
18
|
+
import { startServer } from "./server.ts";
|
|
19
|
+
import { watchWorkspace } from "./watch.ts";
|
|
20
|
+
|
|
21
|
+
function colorEnabled(stream: { isTTY?: boolean }): boolean {
|
|
22
|
+
if (process.env.NO_COLOR) return false;
|
|
23
|
+
const force = process.env.FORCE_COLOR;
|
|
24
|
+
if (force !== undefined && force !== "" && force !== "0") return true;
|
|
25
|
+
return Boolean(stream.isTTY);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function paint(stream: { isTTY?: boolean }, code: string, text: string): string {
|
|
29
|
+
return colorEnabled(stream) ? `\u001b[${code}m${text}\u001b[0m` : text;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function log(message: string): void {
|
|
33
|
+
console.log(`[backeneko] ${message}`);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function warn(message: string): void {
|
|
37
|
+
console.warn(paint(process.stderr, "1;33", `[backeneko] warning: ${message}`));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function logError(message: string): void {
|
|
41
|
+
console.error(paint(process.stderr, "1;31", `[backeneko] error: ${message}`));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function fail(message: string): never {
|
|
45
|
+
console.error(paint(process.stderr, "1;31", `[backeneko] error: ${message}`));
|
|
46
|
+
process.exit(1);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const LOGO = [
|
|
50
|
+
" /\\_/\\",
|
|
51
|
+
" ( =.= ) ( (",
|
|
52
|
+
" / \\ / /",
|
|
53
|
+
" (_|__|_)/_/",
|
|
54
|
+
].join("\n");
|
|
55
|
+
|
|
56
|
+
function printLogo(): void {
|
|
57
|
+
console.log(paint(process.stdout, "36", LOGO));
|
|
58
|
+
console.log("");
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async function main(): Promise<void> {
|
|
62
|
+
printLogo();
|
|
63
|
+
|
|
64
|
+
const options = parseCliArgs();
|
|
65
|
+
|
|
66
|
+
if (options.help) {
|
|
67
|
+
printHelp();
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const cwd = process.cwd();
|
|
72
|
+
const targetDir = path.resolve(cwd, options.dir);
|
|
73
|
+
const schemaPath = path.join(targetDir, "schema.graphql");
|
|
74
|
+
|
|
75
|
+
if (options.source) {
|
|
76
|
+
await generateFromSource(path.resolve(cwd, options.source), targetDir);
|
|
77
|
+
} else if (!existsSync(targetDir)) {
|
|
78
|
+
printHelp();
|
|
79
|
+
fail(`--source is required when ${options.dir} does not exist`);
|
|
80
|
+
} else {
|
|
81
|
+
log(`serving existing ${options.dir} as-is (no --source, schema not regenerated)`);
|
|
82
|
+
await ensureWorkspaceTsconfig(targetDir);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (!existsSync(schemaPath)) {
|
|
86
|
+
fail(`no schema found at ${path.relative(cwd, schemaPath)}`);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
await loadSchema(schemaPath);
|
|
90
|
+
runtime.modules = await loadMockModules(targetDir);
|
|
91
|
+
await logSummary(targetDir);
|
|
92
|
+
|
|
93
|
+
const server = startServer(options.port, { validate: options.validate });
|
|
94
|
+
log(`mock GraphQL backend on http://localhost:${server.port}/graphql`);
|
|
95
|
+
if (!options.validate) warn("query validation disabled");
|
|
96
|
+
log(`hot reload watching ${path.relative(cwd, targetDir)}`);
|
|
97
|
+
|
|
98
|
+
watchWorkspace(targetDir, async (changed) => {
|
|
99
|
+
const rel = changed.map((file) => path.relative(cwd, path.join(targetDir, file))).join(", ");
|
|
100
|
+
if (changed.some((file) => path.basename(file) === "schema.graphql")) {
|
|
101
|
+
try {
|
|
102
|
+
await loadSchema(schemaPath);
|
|
103
|
+
log(`schema reloaded (${rel})`);
|
|
104
|
+
} catch (error) {
|
|
105
|
+
console.error(
|
|
106
|
+
paint(
|
|
107
|
+
process.stderr,
|
|
108
|
+
"1;31",
|
|
109
|
+
`[backeneko] schema reload failed: ${error instanceof Error ? error.message : error}`,
|
|
110
|
+
),
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
const previous = runtime.modules;
|
|
115
|
+
runtime.modules = await loadMockModules(targetDir, previous);
|
|
116
|
+
log(`mocks reloaded (${rel})`);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
process.on("SIGINT", () => {
|
|
120
|
+
server.stop();
|
|
121
|
+
process.exit(0);
|
|
122
|
+
});
|
|
123
|
+
process.on("SIGTERM", () => {
|
|
124
|
+
server.stop();
|
|
125
|
+
process.exit(0);
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async function generateFromSource(sourceDir: string, targetDir: string): Promise<void> {
|
|
130
|
+
if (!existsSync(sourceDir)) {
|
|
131
|
+
fail(`source directory not found: ${sourceDir}`);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const files = await collectSchemaFiles(sourceDir);
|
|
135
|
+
if (files.length === 0) {
|
|
136
|
+
fail(`no .graphql/.gql files found in ${sourceDir}`);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const merged = mergeSchemaDocuments(files);
|
|
140
|
+
for (const error of merged.errors) logError(error);
|
|
141
|
+
if (merged.skippedExecutables.length > 0) {
|
|
142
|
+
log(`ignored ${merged.skippedExecutables.length} executable definition(s) (queries/fragments are not part of the schema)`);
|
|
143
|
+
for (const entry of merged.skippedExecutables) warn(`skipped ${entry}`);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const compiled = compileDocument(merged.document);
|
|
147
|
+
for (const entry of compiled.skipped) warn(`skipped "${entry.name}": ${entry.reason}`);
|
|
148
|
+
for (const warning of compiled.warnings) warn(warning);
|
|
149
|
+
|
|
150
|
+
const sdl = renderSdl(
|
|
151
|
+
compiled.document,
|
|
152
|
+
files.map((file) => file.relativePath),
|
|
153
|
+
compiled.skipped,
|
|
154
|
+
);
|
|
155
|
+
const result = await generateWorkspace({ targetDir, sdl });
|
|
156
|
+
|
|
157
|
+
for (const warning of result.warnings) warn(warning);
|
|
158
|
+
log(`collected ${files.length} schema file(s) from ${sourceDir}`);
|
|
159
|
+
log(`generated ${path.basename(result.schemaPath)}`);
|
|
160
|
+
log(
|
|
161
|
+
`${GENERATED_DIR}/: ${result.created.length} created, ${result.updated.length} updated, ` +
|
|
162
|
+
`${result.unchanged.length} unchanged, ${result.removed.length} removed`,
|
|
163
|
+
);
|
|
164
|
+
if (result.removed.length > 0) log(`pruned ${result.removed.join(", ")}`);
|
|
165
|
+
log(`${OVERRIDE_DIR}/ is never written`);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
async function logSummary(targetDir: string): Promise<void> {
|
|
169
|
+
const { entities, root } = runtime.modules;
|
|
170
|
+
const parts = [`${entities.size} entit${entities.size === 1 ? "y" : "ies"}`];
|
|
171
|
+
for (const [typeName, resolvers] of root) {
|
|
172
|
+
parts.push(`${resolvers.size} ${typeName.toLowerCase()} resolver(s)`);
|
|
173
|
+
}
|
|
174
|
+
log(`loaded ${parts.join(", ")} from ${path.relative(process.cwd(), targetDir)}`);
|
|
175
|
+
|
|
176
|
+
const overrides = await countFiles(path.join(targetDir, OVERRIDE_DIR));
|
|
177
|
+
if (overrides > 0) {
|
|
178
|
+
log(`${overrides} override file(s) in ${path.relative(process.cwd(), path.join(targetDir, OVERRIDE_DIR))}`);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
async function countFiles(directory: string): Promise<number> {
|
|
183
|
+
const entries = await readdir(directory, { withFileTypes: true }).catch(() => []);
|
|
184
|
+
let count = 0;
|
|
185
|
+
for (const entry of entries) {
|
|
186
|
+
if (entry.isDirectory()) count += await countFiles(path.join(directory, entry.name));
|
|
187
|
+
else count += 1;
|
|
188
|
+
}
|
|
189
|
+
return count;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
async function loadSchema(schemaPath: string): Promise<void> {
|
|
193
|
+
const sdl = await readFile(schemaPath, "utf8");
|
|
194
|
+
runtime.schema = buildExecutableSchema(sdl);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
main().catch((error) => {
|
|
198
|
+
fail(error instanceof Error ? error.message : String(error));
|
|
199
|
+
});
|
package/src/collect.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { readFile, readdir } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
const SCHEMA_EXTENSIONS = new Set([".graphql", ".gql"]);
|
|
5
|
+
const IGNORED_DIRECTORIES = new Set(["node_modules", ".git", "dist"]);
|
|
6
|
+
|
|
7
|
+
export interface SchemaSourceFile {
|
|
8
|
+
absolutePath: string;
|
|
9
|
+
relativePath: string;
|
|
10
|
+
content: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export async function collectSchemaFiles(root: string): Promise<SchemaSourceFile[]> {
|
|
14
|
+
const files: SchemaSourceFile[] = [];
|
|
15
|
+
|
|
16
|
+
async function walk(dir: string, prefix: string): Promise<void> {
|
|
17
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
18
|
+
entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
19
|
+
for (const entry of entries) {
|
|
20
|
+
const relative = prefix + entry.name;
|
|
21
|
+
if (entry.isDirectory()) {
|
|
22
|
+
if (entry.name.startsWith(".") || IGNORED_DIRECTORIES.has(entry.name)) continue;
|
|
23
|
+
await walk(path.join(dir, entry.name), relative + "/");
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
if (!entry.isFile() || !SCHEMA_EXTENSIONS.has(path.extname(entry.name))) continue;
|
|
27
|
+
const absolutePath = path.join(dir, entry.name);
|
|
28
|
+
files.push({
|
|
29
|
+
absolutePath,
|
|
30
|
+
relativePath: relative,
|
|
31
|
+
content: await readFile(absolutePath, "utf8"),
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
await walk(root, "");
|
|
37
|
+
return files;
|
|
38
|
+
}
|