@fougere/testing 0.3.0-alpha.0 → 0.5.0-alpha.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/dist/app.js +3 -3
- package/dist/app.js.map +1 -1
- package/dist/comparison.d.ts.map +1 -1
- package/dist/comparison.js +3 -3
- package/dist/comparison.js.map +1 -1
- package/dist/derive.d.ts +2 -2
- package/dist/derive.d.ts.map +1 -1
- package/dist/derive.js +3 -3
- package/dist/derive.js.map +1 -1
- package/dist/doors.d.ts +1 -1
- package/dist/doors.d.ts.map +1 -1
- package/dist/doors.js +6 -6
- package/dist/doors.js.map +1 -1
- package/dist/gql.d.ts.map +1 -1
- package/dist/gql.js +2 -2
- package/dist/gql.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/remotes.d.ts +4 -45
- package/dist/remotes.d.ts.map +1 -1
- package/dist/remotes.js +4 -117
- package/dist/remotes.js.map +1 -1
- package/dist/sample.d.ts +1 -1
- package/dist/sample.d.ts.map +1 -1
- package/dist/sample.js +2 -2
- package/dist/sample.js.map +1 -1
- package/dist/sync.d.ts +3 -3
- package/dist/sync.d.ts.map +1 -1
- package/dist/sync.js +3 -3
- package/dist/sync.js.map +1 -1
- package/dist/vitest.d.ts.map +1 -1
- package/dist/vitest.js +1 -3
- package/dist/vitest.js.map +1 -1
- package/package.json +11 -10
- package/src/all.ts +57 -0
- package/src/app.ts +125 -0
- package/src/comparison.ts +237 -0
- package/src/derive.ts +18 -0
- package/src/doors.ts +103 -0
- package/src/gql.ts +136 -0
- package/src/index.ts +20 -0
- package/src/load.ts +107 -0
- package/src/remotes.ts +7 -0
- package/src/sample.ts +86 -0
- package/src/scope.ts +81 -0
- package/src/stub.ts +80 -0
- package/src/sync.ts +98 -0
- package/src/vitest.ts +48 -0
package/src/sync.ts
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { pathToFileURL } from 'node:url';
|
|
4
|
+
import { Card, type Change, type SchemaDescriptor, type SchemaView } from '@fougere/schema';
|
|
5
|
+
import type { IdentityCard } from '@fougere/core';
|
|
6
|
+
|
|
7
|
+
/** One entry of `.fougere/remotes.json`, written by `fougere sync`. */
|
|
8
|
+
export interface SyncedRemote {
|
|
9
|
+
name: string;
|
|
10
|
+
url: string;
|
|
11
|
+
/** Where the synced classes were written. */
|
|
12
|
+
path: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** What separates a consumer's synced copy from what the producer serves today. */
|
|
16
|
+
export interface SyncDrift {
|
|
17
|
+
frond: string;
|
|
18
|
+
/** An entity the consumer holds that the producer no longer serves. */
|
|
19
|
+
gone: string[];
|
|
20
|
+
/** A shape that moved under a name the consumer still holds. */
|
|
21
|
+
moved: { entity: string; changes: Change[] }[];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** The remotes a project synced, read from the file `fougere sync` writes. */
|
|
25
|
+
export async function syncedRemotes(root: string): Promise<SyncedRemote[]> {
|
|
26
|
+
try {
|
|
27
|
+
const raw = await readFile(join(root, '.fougere', 'remotes.json'), 'utf8');
|
|
28
|
+
const parsed = JSON.parse(raw) as Record<string, { url: string; path: string }>;
|
|
29
|
+
return Object.entries(parsed).map(([name, one]) => ({ name, ...one }));
|
|
30
|
+
} catch {
|
|
31
|
+
// No file is the ordinary case: an app with no remote synced nothing.
|
|
32
|
+
return [];
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* The shapes a consumer holds for one remote frond.
|
|
38
|
+
*
|
|
39
|
+
* Rebuilt from the classes rather than read back: `fougere sync` writes entities and an
|
|
40
|
+
* index and does NOT keep the card it received. So the consumer's copy of the OPERATIONS
|
|
41
|
+
* has no local counterpart at all, and only the shapes can be compared — which is the
|
|
42
|
+
* narrower half, and the one that breaks a caller silently.
|
|
43
|
+
*
|
|
44
|
+
* Closing that would mean `sync` writing the card beside the classes; it is a change to
|
|
45
|
+
* the CLI, not to this file.
|
|
46
|
+
*/
|
|
47
|
+
export async function heldShapes(remote: SyncedRemote): Promise<Map<string, SchemaDescriptor>> {
|
|
48
|
+
const held = new Map<string, SchemaDescriptor>();
|
|
49
|
+
const index = pathToFileURL(join(remote.path, 'index.ts')).href;
|
|
50
|
+
const module = await import(index) as Record<string, unknown>;
|
|
51
|
+
|
|
52
|
+
for (const [name, exported] of Object.entries(module)) {
|
|
53
|
+
const entity = exported as SchemaView | undefined;
|
|
54
|
+
if (typeof entity !== 'function' || typeof (entity as SchemaView).getFields !== 'function') continue;
|
|
55
|
+
held.set(name, Card.fromSchema(entity).descriptor);
|
|
56
|
+
}
|
|
57
|
+
return held;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** The shapes a card announces, by the name a door carries. */
|
|
61
|
+
function servedShapes(card: IdentityCard, frond: string): Map<string, unknown> {
|
|
62
|
+
const served = new Map<string, unknown>();
|
|
63
|
+
for (const one of card.fronds) {
|
|
64
|
+
if (one.name !== frond) continue;
|
|
65
|
+
for (const door of one.doors) if (door.schema) served.set(door.schema.title ?? door.name, door.schema);
|
|
66
|
+
}
|
|
67
|
+
return served;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* What the consumer's copy no longer matches.
|
|
72
|
+
*
|
|
73
|
+
* Compared by the shape's own `title` — the entity's name as the descriptor carries it —
|
|
74
|
+
* rather than by the door's, because a door is addressed in lowercase (`post`) and a class
|
|
75
|
+
* is not (`Post`), and matching those two by hand is where a name convention gets copied
|
|
76
|
+
* a third time.
|
|
77
|
+
*/
|
|
78
|
+
export function syncDriftOf(
|
|
79
|
+
held: Map<string, SchemaDescriptor>,
|
|
80
|
+
card: IdentityCard,
|
|
81
|
+
frond: string,
|
|
82
|
+
): SyncDrift {
|
|
83
|
+
const served = servedShapes(card, frond);
|
|
84
|
+
const drift: SyncDrift = { frond, gone: [], moved: [] };
|
|
85
|
+
|
|
86
|
+
for (const [name, mine] of held) {
|
|
87
|
+
const theirs = served.get(name) ?? served.get(name.toLowerCase());
|
|
88
|
+
if (!theirs) { drift.gone.push(name); continue; }
|
|
89
|
+
const moved = Card.fromDescriptor(mine).diff(Card.fromDescriptor(theirs as SchemaDescriptor));
|
|
90
|
+
if (moved.changes.length > 0) drift.moved.push({ entity: name, changes: moved.changes });
|
|
91
|
+
}
|
|
92
|
+
return drift;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Whether the copy still matches. */
|
|
96
|
+
export function inSync(drift: SyncDrift): boolean {
|
|
97
|
+
return drift.gone.length === 0 && drift.moved.length === 0;
|
|
98
|
+
}
|
package/src/vitest.ts
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { createRequire } from 'node:module';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The vitest configuration a Fougere project needs, so nobody has to discover it.
|
|
5
|
+
*
|
|
6
|
+
* One thing to know, and it is not obvious: `graphql` guards its types with `instanceOf`,
|
|
7
|
+
* so a schema built by one loaded copy is refused by another with *"from another module
|
|
8
|
+
* or realm"*. Under vitest a module is either transformed by Vite or externalized and
|
|
9
|
+
* loaded by node, and Pothos ending up on one side while `graphql()` sits on the other
|
|
10
|
+
* makes two copies of the same file. The alias pins the path, and the inlining makes the
|
|
11
|
+
* packages that touch it take that path — neither alone is enough, which is what made
|
|
12
|
+
* this cost an afternoon.
|
|
13
|
+
*
|
|
14
|
+
* An app that serves no GraphQL pays nothing for this: the alias resolves to whatever
|
|
15
|
+
* `graphql` the project has, and no `graphql` means the option is simply unused.
|
|
16
|
+
*
|
|
17
|
+
* ```ts
|
|
18
|
+
* // vitest.config.ts
|
|
19
|
+
* import { fougereTest } from '@fougere/testing/vitest';
|
|
20
|
+
* export default fougereTest();
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
export function fougereTest(overrides: Record<string, unknown> = {}): Record<string, unknown> {
|
|
24
|
+
const require = createRequire(import.meta.url);
|
|
25
|
+
let graphqlDir: string | undefined;
|
|
26
|
+
try {
|
|
27
|
+
graphqlDir = require.resolve('graphql').replace(/\/index\.js$/, '');
|
|
28
|
+
} catch {
|
|
29
|
+
// No GraphQL in this project — nothing to pin.
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const base = {
|
|
33
|
+
resolve: (graphqlDir ? { alias: { graphql: graphqlDir }, dedupe: ['graphql'] } : {}),
|
|
34
|
+
test: {
|
|
35
|
+
// Both spellings of where a test lives: beside the frond it is about, and above
|
|
36
|
+
// the fronds when it is about several of them.
|
|
37
|
+
include: ['tests/**/*.test.ts', 'fronds/*/tests/**/*.test.ts'],
|
|
38
|
+
server: { deps: { inline: [/@fougere\//, '@pothos/core'] } },
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
...base,
|
|
44
|
+
...overrides,
|
|
45
|
+
resolve: { ...base.resolve, ...(overrides.resolve as object ?? {}) },
|
|
46
|
+
test: { ...base.test, ...(overrides.test as object ?? {}) },
|
|
47
|
+
};
|
|
48
|
+
}
|