@proteinjs/reflection-build 2.0.3 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (176) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/dist/src/ReflectionDoctor.d.ts +6 -1
  3. package/dist/src/ReflectionDoctor.js +53 -51
  4. package/dist/src/ReflectionDoctor.js.map +1 -1
  5. package/dist/src/codegen/CanonicalSourceGraph.d.ts +22 -0
  6. package/dist/src/codegen/CanonicalSourceGraph.js +60 -0
  7. package/dist/src/codegen/CanonicalSourceGraph.js.map +1 -0
  8. package/dist/src/codegen/writeGeneratedIndex.d.ts +12 -3
  9. package/dist/src/codegen/writeGeneratedIndex.js +21 -13
  10. package/dist/src/codegen/writeGeneratedIndex.js.map +1 -1
  11. package/dist/src/parser/PackageSourceFiles.d.ts +30 -0
  12. package/dist/src/parser/PackageSourceFiles.js +151 -0
  13. package/dist/src/parser/PackageSourceFiles.js.map +1 -0
  14. package/dist/src/parser/SharedQualifiedNames.d.ts +43 -0
  15. package/dist/src/parser/SharedQualifiedNames.js +81 -0
  16. package/dist/src/parser/SharedQualifiedNames.js.map +1 -0
  17. package/dist/src/parser/createGraphBuilder.d.ts +6 -1
  18. package/dist/src/parser/createGraphBuilder.js +16 -31
  19. package/dist/src/parser/createGraphBuilder.js.map +1 -1
  20. package/dist/src/parser/createSourceGraph.d.ts +6 -1
  21. package/dist/src/parser/createSourceGraph.js +23 -38
  22. package/dist/src/parser/createSourceGraph.js.map +1 -1
  23. package/dist/test/Class.test.js +1 -1
  24. package/dist/test/Class.test.js.map +1 -1
  25. package/dist/test/ExamplePackageFixture.d.ts +47 -0
  26. package/dist/test/ExamplePackageFixture.js +207 -0
  27. package/dist/test/ExamplePackageFixture.js.map +1 -0
  28. package/dist/test/GraphEmitRelativePaths.test.js +1 -1
  29. package/dist/test/GraphEmitRelativePaths.test.js.map +1 -1
  30. package/dist/test/PackageSourceFiles.test.d.ts +1 -0
  31. package/dist/test/PackageSourceFiles.test.js +57 -0
  32. package/dist/test/PackageSourceFiles.test.js.map +1 -0
  33. package/dist/test/ReflectionDoctor.test.js +55 -18
  34. package/dist/test/ReflectionDoctor.test.js.map +1 -1
  35. package/dist/test/ReproducibleGeneratedIndex.test.d.ts +1 -0
  36. package/dist/test/ReproducibleGeneratedIndex.test.js +224 -0
  37. package/dist/test/ReproducibleGeneratedIndex.test.js.map +1 -0
  38. package/dist/test/SharedQualifiedNames.test.d.ts +1 -0
  39. package/dist/test/SharedQualifiedNames.test.js +186 -0
  40. package/dist/test/SharedQualifiedNames.test.js.map +1 -0
  41. package/dist/test/examples/reproducible/index.d.ts +4 -0
  42. package/dist/test/examples/reproducible/index.js +21 -0
  43. package/dist/test/examples/reproducible/index.js.map +1 -0
  44. package/dist/test/examples/reproducible/src/alpha/Alpha.d.ts +10 -0
  45. package/dist/test/examples/reproducible/src/alpha/Alpha.js +18 -0
  46. package/dist/test/examples/reproducible/src/alpha/Alpha.js.map +1 -0
  47. package/dist/test/examples/reproducible/src/alpha/Defaults.d.ts +2 -0
  48. package/dist/test/examples/reproducible/src/alpha/Defaults.js +6 -0
  49. package/dist/test/examples/reproducible/src/alpha/Defaults.js.map +1 -0
  50. package/dist/test/examples/reproducible/src/beta/Beta.d.ts +5 -0
  51. package/dist/test/examples/reproducible/src/beta/Beta.js +12 -0
  52. package/dist/test/examples/reproducible/src/beta/Beta.js.map +1 -0
  53. package/dist/test/examples/reproducible/src/beta/deep/Gamma.d.ts +4 -0
  54. package/dist/test/examples/reproducible/src/beta/deep/Gamma.js +11 -0
  55. package/dist/test/examples/reproducible/src/beta/deep/Gamma.js.map +1 -0
  56. package/dist/test/examples/reproducible/src/zeta/Defaults.d.ts +2 -0
  57. package/dist/test/examples/reproducible/src/zeta/Defaults.js +6 -0
  58. package/dist/test/examples/reproducible/src/zeta/Defaults.js.map +1 -0
  59. package/dist/test/examples/reproducible/src/zeta/Zeta.d.ts +7 -0
  60. package/dist/test/examples/reproducible/src/zeta/Zeta.js +18 -0
  61. package/dist/test/examples/reproducible/src/zeta/Zeta.js.map +1 -0
  62. package/dist/test/examples/shared-name/index.d.ts +1 -0
  63. package/dist/test/examples/shared-name/index.js +18 -0
  64. package/dist/test/examples/shared-name/index.js.map +1 -0
  65. package/dist/test/examples/shared-name/src/alpha/Widget.d.ts +4 -0
  66. package/dist/test/examples/shared-name/src/alpha/Widget.js +3 -0
  67. package/dist/test/examples/shared-name/src/alpha/Widget.js.map +1 -0
  68. package/dist/test/examples/shared-name/src/beta/Widget.d.ts +3 -0
  69. package/dist/test/examples/shared-name/src/beta/Widget.js +13 -0
  70. package/dist/test/examples/shared-name/src/beta/Widget.js.map +1 -0
  71. package/dist/test/examples/shared-name/src/gamma/Gear.d.ts +7 -0
  72. package/dist/test/examples/shared-name/src/gamma/Gear.js +5 -0
  73. package/dist/test/examples/shared-name/src/gamma/Gear.js.map +1 -0
  74. package/dist/test/examples/shared-name-pruned/index.d.ts +1 -0
  75. package/dist/test/examples/shared-name-pruned/index.js +18 -0
  76. package/dist/test/examples/shared-name-pruned/index.js.map +1 -0
  77. package/dist/test/examples/shared-name-pruned/src/alpha/Limits.d.ts +3 -0
  78. package/dist/test/examples/shared-name-pruned/src/alpha/Limits.js +6 -0
  79. package/dist/test/examples/shared-name-pruned/src/alpha/Limits.js.map +1 -0
  80. package/dist/test/examples/shared-name-pruned/src/alpha/Shape.d.ts +3 -0
  81. package/dist/test/examples/shared-name-pruned/src/alpha/Shape.js +3 -0
  82. package/dist/test/examples/shared-name-pruned/src/alpha/Shape.js.map +1 -0
  83. package/dist/test/examples/shared-name-pruned/src/alpha/Tile.d.ts +7 -0
  84. package/dist/test/examples/shared-name-pruned/src/alpha/Tile.js +11 -0
  85. package/dist/test/examples/shared-name-pruned/src/alpha/Tile.js.map +1 -0
  86. package/dist/test/examples/shared-name-pruned/src/beta/Limits.d.ts +3 -0
  87. package/dist/test/examples/shared-name-pruned/src/beta/Limits.js +6 -0
  88. package/dist/test/examples/shared-name-pruned/src/beta/Limits.js.map +1 -0
  89. package/dist/test/examples/shared-name-pruned/src/beta/Shape.d.ts +3 -0
  90. package/dist/test/examples/shared-name-pruned/src/beta/Shape.js +3 -0
  91. package/dist/test/examples/shared-name-pruned/src/beta/Shape.js.map +1 -0
  92. package/dist/test/examples/source-repository/a/generated/index.js +28 -28
  93. package/dist/test/examples/source-repository/a/generated/index.js.map +1 -1
  94. package/dist/test/examples/source-repository/a/src/ExtendsForeignType.d.ts +1 -1
  95. package/dist/test/examples/source-repository/a/src/ExtendsForeignType.js +5 -5
  96. package/dist/test/examples/source-repository/a/src/ExtendsForeignType.js.map +1 -1
  97. package/dist/test/examples/source-repository/b/generated/index.js +3 -3
  98. package/dist/test/examples/source-repository/b/generated/index.js.map +1 -1
  99. package/package.json +2 -2
  100. package/src/ReflectionDoctor.ts +37 -17
  101. package/src/codegen/CanonicalSourceGraph.ts +50 -0
  102. package/src/codegen/writeGeneratedIndex.ts +35 -9
  103. package/src/parser/PackageSourceFiles.ts +66 -0
  104. package/src/parser/SharedQualifiedNames.ts +76 -0
  105. package/src/parser/createGraphBuilder.ts +25 -10
  106. package/src/parser/createSourceGraph.ts +14 -16
  107. package/test/Class.test.ts +1 -1
  108. package/test/ExamplePackageFixture.ts +142 -0
  109. package/test/GraphEmitRelativePaths.test.ts +1 -1
  110. package/test/PackageSourceFiles.test.ts +42 -0
  111. package/test/ReflectionDoctor.test.ts +50 -12
  112. package/test/ReproducibleGeneratedIndex.test.ts +114 -0
  113. package/test/SharedQualifiedNames.test.ts +89 -0
  114. package/test/examples/reproducible/index.ts +4 -0
  115. package/test/examples/reproducible/src/alpha/Alpha.ts +13 -0
  116. package/test/examples/reproducible/src/alpha/Defaults.ts +4 -0
  117. package/test/examples/reproducible/src/beta/Beta.ts +7 -0
  118. package/test/examples/reproducible/src/beta/deep/Gamma.ts +5 -0
  119. package/test/examples/reproducible/src/zeta/Defaults.ts +4 -0
  120. package/test/examples/reproducible/src/zeta/Zeta.ts +9 -0
  121. package/test/examples/shared-name/index.ts +1 -0
  122. package/test/examples/shared-name/src/alpha/Widget.ts +6 -0
  123. package/test/examples/shared-name/src/beta/Widget.ts +5 -0
  124. package/test/examples/shared-name/src/gamma/Gear.ts +9 -0
  125. package/test/examples/shared-name-pruned/index.ts +1 -0
  126. package/test/examples/shared-name-pruned/src/alpha/Limits.ts +2 -0
  127. package/test/examples/shared-name-pruned/src/alpha/Shape.ts +3 -0
  128. package/test/examples/shared-name-pruned/src/alpha/Tile.ts +9 -0
  129. package/test/examples/shared-name-pruned/src/beta/Limits.ts +2 -0
  130. package/test/examples/shared-name-pruned/src/beta/Shape.ts +4 -0
  131. package/test/examples/source-repository/a/dist/codegen/CanonicalSourceGraph.d.ts +22 -0
  132. package/test/examples/source-repository/a/dist/codegen/CanonicalSourceGraph.js +60 -0
  133. package/test/examples/source-repository/a/dist/codegen/CanonicalSourceGraph.js.map +1 -0
  134. package/test/examples/source-repository/a/dist/codegen/writeGeneratedIndex.d.ts +12 -3
  135. package/test/examples/source-repository/a/dist/codegen/writeGeneratedIndex.js +21 -13
  136. package/test/examples/source-repository/a/dist/codegen/writeGeneratedIndex.js.map +1 -1
  137. package/test/examples/source-repository/a/dist/generated/index.js +28 -28
  138. package/test/examples/source-repository/a/dist/generated/index.js.map +1 -1
  139. package/test/examples/source-repository/a/dist/parser/PackageSourceFiles.d.ts +30 -0
  140. package/test/examples/source-repository/a/dist/parser/PackageSourceFiles.js +151 -0
  141. package/test/examples/source-repository/a/dist/parser/PackageSourceFiles.js.map +1 -0
  142. package/test/examples/source-repository/a/dist/parser/SharedQualifiedNames.d.ts +43 -0
  143. package/test/examples/source-repository/a/dist/parser/SharedQualifiedNames.js +81 -0
  144. package/test/examples/source-repository/a/dist/parser/SharedQualifiedNames.js.map +1 -0
  145. package/test/examples/source-repository/a/dist/parser/createGraphBuilder.d.ts +6 -1
  146. package/test/examples/source-repository/a/dist/parser/createGraphBuilder.js +16 -31
  147. package/test/examples/source-repository/a/dist/parser/createGraphBuilder.js.map +1 -1
  148. package/test/examples/source-repository/a/dist/parser/createSourceGraph.d.ts +6 -1
  149. package/test/examples/source-repository/a/dist/parser/createSourceGraph.js +23 -38
  150. package/test/examples/source-repository/a/dist/parser/createSourceGraph.js.map +1 -1
  151. package/test/examples/source-repository/a/dist/src/ExtendsForeignType.d.ts +1 -1
  152. package/test/examples/source-repository/a/dist/src/ExtendsForeignType.js +5 -5
  153. package/test/examples/source-repository/a/dist/src/ExtendsForeignType.js.map +1 -1
  154. package/test/examples/source-repository/a/generated/index.ts +44 -44
  155. package/test/examples/source-repository/a/src/ExtendsForeignType.ts +1 -1
  156. package/test/examples/source-repository/b/dist/codegen/CanonicalSourceGraph.d.ts +22 -0
  157. package/test/examples/source-repository/b/dist/codegen/CanonicalSourceGraph.js +60 -0
  158. package/test/examples/source-repository/b/dist/codegen/CanonicalSourceGraph.js.map +1 -0
  159. package/test/examples/source-repository/b/dist/codegen/writeGeneratedIndex.d.ts +12 -3
  160. package/test/examples/source-repository/b/dist/codegen/writeGeneratedIndex.js +21 -13
  161. package/test/examples/source-repository/b/dist/codegen/writeGeneratedIndex.js.map +1 -1
  162. package/test/examples/source-repository/b/dist/generated/index.js +3 -3
  163. package/test/examples/source-repository/b/dist/generated/index.js.map +1 -1
  164. package/test/examples/source-repository/b/dist/parser/PackageSourceFiles.d.ts +30 -0
  165. package/test/examples/source-repository/b/dist/parser/PackageSourceFiles.js +151 -0
  166. package/test/examples/source-repository/b/dist/parser/PackageSourceFiles.js.map +1 -0
  167. package/test/examples/source-repository/b/dist/parser/SharedQualifiedNames.d.ts +43 -0
  168. package/test/examples/source-repository/b/dist/parser/SharedQualifiedNames.js +81 -0
  169. package/test/examples/source-repository/b/dist/parser/SharedQualifiedNames.js.map +1 -0
  170. package/test/examples/source-repository/b/dist/parser/createGraphBuilder.d.ts +6 -1
  171. package/test/examples/source-repository/b/dist/parser/createGraphBuilder.js +16 -31
  172. package/test/examples/source-repository/b/dist/parser/createGraphBuilder.js.map +1 -1
  173. package/test/examples/source-repository/b/dist/parser/createSourceGraph.d.ts +6 -1
  174. package/test/examples/source-repository/b/dist/parser/createSourceGraph.js +23 -38
  175. package/test/examples/source-repository/b/dist/parser/createSourceGraph.js.map +1 -1
  176. package/test/examples/source-repository/b/generated/index.ts +6 -6
@@ -1,14 +1,20 @@
1
1
  import * as graphlib from '@dagrejs/graphlib';
2
- import globby from 'globby';
3
2
  import * as path from 'path';
4
3
  import { TypescriptParser } from '../../modules/typescript-parser';
5
4
  import { promisifiedFs } from '@proteinjs/util-node';
6
5
  import { createGraphBuilder } from './createGraphBuilder';
6
+ import { PackageSourceFiles } from './PackageSourceFiles';
7
+ import { SharedQualifiedNames } from './SharedQualifiedNames';
7
8
 
9
+ /**
10
+ * @param sharedNames receives the file of every declaration the graph holds a node for; the
11
+ * build reads the names two files declare from it after the prune
12
+ */
8
13
  export async function createSourceGraph(
9
14
  dir: string,
10
15
  excludedDirs: string[] = [],
11
- sourceRootsRel: string | string[] = 'src'
16
+ sourceRootsRel: string | string[] = 'src',
17
+ sharedNames: SharedQualifiedNames = new SharedQualifiedNames()
12
18
  ) {
13
19
  const packageJsonPath = path.join(dir, 'package.json');
14
20
  if (!(await promisifiedFs.exists(packageJsonPath))) {
@@ -17,24 +23,16 @@ export async function createSourceGraph(
17
23
 
18
24
  const packageJson = require(packageJsonPath);
19
25
  const roots = Array.isArray(sourceRootsRel) ? sourceRootsRel : [sourceRootsRel];
20
-
21
- const patterns: string[] = [];
22
- for (const rel of roots) {
23
- const root = path.join(dir, rel);
24
- patterns.push(path.join(root, '**/*.ts'));
25
- patterns.push(path.join(root, '**/*.tsx'));
26
- }
27
-
28
- const excludePatterns = ['!**/node_modules/**', '!**/generated/**', ...excludedDirs.map((d) => `!${d}`)];
29
-
30
- const sourceFilePaths = await globby([...patterns, ...excludePatterns]);
31
- const uniqueFilePaths = Array.from(new Set(sourceFilePaths));
26
+ const sourceFilePaths = await new PackageSourceFiles(dir, roots, excludedDirs).list();
32
27
 
33
28
  const graph = new graphlib.Graph();
34
- const addSourceFile = createGraphBuilder(graph, packageJson, dir);
29
+ const addSourceFile = createGraphBuilder(graph, packageJson, dir, sharedNames);
35
30
  const parser = new TypescriptParser();
36
31
 
37
- for (const sourceFilePath of uniqueFilePaths) {
32
+ // Sequential, in PackageSourceFiles order: the parse order is part of the graph's content (a
33
+ // name exported by two files holds the declaration parsed last — the build refuses such a name
34
+ // when it survives the prune) and the order `sharedNames` lists the declaring files in.
35
+ for (const sourceFilePath of sourceFilePaths) {
38
36
  const sourceFile = await parser.parseFile(sourceFilePath, path.dirname(sourceFilePath));
39
37
  await addSourceFile(sourceFile);
40
38
  }
@@ -60,7 +60,7 @@ test('do not load non-loadable classes', async () => {
60
60
  ).toBeUndefined();
61
61
  expect(
62
62
  sourceRepository.flattenedSourceGraph.classes[
63
- '@proteinjs/reflection-build-test-a/ImplementsExtendsLocalNotLoadableInterface'
63
+ '@proteinjs/reflection-build-test-a/ImplementsExtendsNotLoadableForeignInterface'
64
64
  ]
65
65
  ).toBeUndefined();
66
66
  expect(
@@ -0,0 +1,142 @@
1
+ import * as crypto from 'crypto';
2
+ import * as fs from 'fs';
3
+ import * as os from 'os';
4
+ import * as path from 'path';
5
+ import { execFileSync } from 'child_process';
6
+ import { build } from '../src/build';
7
+
8
+ /**
9
+ * Disposable copies of an example package under `test/examples`, each at its own absolute
10
+ * location, built the way the `reflection-build` bin builds a package (the package dir arrives
11
+ * as INIT_CWD), compiled with tsc and packed with `npm pack` — so a test can compare what two
12
+ * builds of the same sources ship, or what one build refuses.
13
+ */
14
+ export class ExamplePackageFixture {
15
+ private static readonly NODE_MODULES = path.join(__dirname, '..', 'node_modules');
16
+ private readonly sources: string;
17
+ private readonly tmpRoot: string;
18
+ private readonly links: string[] = [];
19
+
20
+ /**
21
+ * @param example the example's directory under `test/examples`
22
+ * @param packageName the name its copies are built under (a fixture name, never a published package's)
23
+ */
24
+ constructor(
25
+ example: string,
26
+ readonly packageName: string
27
+ ) {
28
+ this.sources = path.join(__dirname, 'examples', example);
29
+ this.tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), `refl-${example}-`));
30
+ }
31
+
32
+ /** `packageName/name`: the qualified name this package declares `name` under. */
33
+ qualified(name: string): string {
34
+ return `${this.packageName}/${name}`;
35
+ }
36
+
37
+ /** Copies the example package to `<tmp>/<location>` and returns its absolute path. */
38
+ materialize(location: string): string {
39
+ const packageDir = path.join(this.tmpRoot, location);
40
+ fs.mkdirSync(path.dirname(packageDir), { recursive: true });
41
+ this.copyTree(this.sources, packageDir);
42
+ fs.writeFileSync(path.join(packageDir, 'package.json'), JSON.stringify(this.packageJson(), null, 2));
43
+ fs.writeFileSync(path.join(packageDir, 'tsconfig.json'), JSON.stringify(this.tsconfig(), null, 2));
44
+ const link = path.join(packageDir, 'node_modules');
45
+ fs.symlinkSync(ExamplePackageFixture.NODE_MODULES, link, 'dir');
46
+ this.links.push(link);
47
+ return packageDir;
48
+ }
49
+
50
+ /** Runs reflection-build in `packageDir`; returns the generated index it wrote (rejects when the build refuses). */
51
+ async generate(packageDir: string): Promise<string> {
52
+ const initCwd = process.env.INIT_CWD;
53
+ process.env.INIT_CWD = packageDir;
54
+ try {
55
+ await build();
56
+ } finally {
57
+ if (initCwd === undefined) {
58
+ delete process.env.INIT_CWD;
59
+ } else {
60
+ process.env.INIT_CWD = initCwd;
61
+ }
62
+ }
63
+ return fs.readFileSync(path.join(packageDir, 'generated', 'index.ts'), 'utf-8');
64
+ }
65
+
66
+ /** tsc over the package (its tsconfig includes the generated index). */
67
+ compile(packageDir: string): void {
68
+ execFileSync(process.execPath, [require.resolve('typescript/bin/tsc'), '-p', packageDir], { stdio: 'pipe' });
69
+ }
70
+
71
+ /** `npm pack` of the package; returns the tarball's sha256. */
72
+ packSha256(packageDir: string): string {
73
+ const destination = fs.mkdtempSync(path.join(this.tmpRoot, 'pack-'));
74
+ execFileSync('npm', ['pack', '--pack-destination', destination], { cwd: packageDir, stdio: 'pipe' });
75
+ const [tarball] = fs.readdirSync(destination);
76
+ return this.sha256(fs.readFileSync(path.join(destination, tarball)));
77
+ }
78
+
79
+ sha256(bytes: Buffer | string): string {
80
+ return crypto.createHash('sha256').update(bytes).digest('hex');
81
+ }
82
+
83
+ /** The source graph a generated index embeds, read the way the runtime reads it. */
84
+ graphOf(generatedIndex: string): { nodes: { v: string; value?: any }[]; edges: { v: string; w: string }[] } {
85
+ const literal = generatedIndex.match(/const sourceGraph = (".*");/);
86
+ if (!literal) {
87
+ throw new Error('no serialized sourceGraph in the generated index');
88
+ }
89
+ return JSON.parse(JSON.parse(literal[1]).replace(/\\'/g, "'"));
90
+ }
91
+
92
+ /** Removes the copies; each linked node_modules is unlinked first, never descended into. */
93
+ dispose(): void {
94
+ for (const link of this.links) {
95
+ fs.unlinkSync(link);
96
+ }
97
+ execFileSync('rm', ['-rf', this.tmpRoot]);
98
+ }
99
+
100
+ private packageJson(): object {
101
+ return {
102
+ name: this.packageName,
103
+ version: '0.0.1',
104
+ private: true,
105
+ main: './dist/generated/index.js',
106
+ types: './dist/generated/index.d.ts',
107
+ files: ['dist/**'],
108
+ dependencies: { '@proteinjs/reflection': '^1.2.1' },
109
+ };
110
+ }
111
+
112
+ private tsconfig(): object {
113
+ return {
114
+ compilerOptions: {
115
+ noEmitOnError: true,
116
+ target: 'es5',
117
+ module: 'commonjs',
118
+ declaration: true,
119
+ sourceMap: true,
120
+ outDir: './dist/',
121
+ strict: true,
122
+ esModuleInterop: true,
123
+ skipLibCheck: true,
124
+ types: [],
125
+ },
126
+ include: ['./index.ts', './generated/index.ts'],
127
+ };
128
+ }
129
+
130
+ private copyTree(from: string, to: string): void {
131
+ fs.mkdirSync(to, { recursive: true });
132
+ for (const entry of fs.readdirSync(from, { withFileTypes: true })) {
133
+ const source = path.join(from, entry.name);
134
+ const target = path.join(to, entry.name);
135
+ if (entry.isDirectory()) {
136
+ this.copyTree(source, target);
137
+ } else {
138
+ fs.copyFileSync(source, target);
139
+ }
140
+ }
141
+ }
142
+ }
@@ -78,7 +78,7 @@ describe('graph emit: package-relative filePaths', () => {
78
78
  }
79
79
  // Fixture a declares sources under src/ — the graph must actually carry them.
80
80
  expect(checked.length).toBeGreaterThan(0);
81
- expect(checked.every((p) => p.startsWith('src' + path.sep) || p.startsWith('index.'))).toBe(true);
81
+ expect(checked.every((p) => p.startsWith('src/') || p.startsWith('index.'))).toBe(true);
82
82
  });
83
83
 
84
84
  it('sourceLink imports still resolve to the linked source files (outcome: links survive)', () => {
@@ -0,0 +1,42 @@
1
+ import * as path from 'path';
2
+ import { PackageSourceFiles } from '../src/parser/PackageSourceFiles';
3
+
4
+ /**
5
+ * The two machine-specific inputs `PackageSourceFiles` closes where they enter the generated
6
+ * index: the platform's path separator and the machine's collation. The Windows half cannot run
7
+ * on the machine that runs this suite, so the class is loaded once more against `path.win32` in
8
+ * an isolated module registry — the same code, Windows paths in.
9
+ */
10
+ describe('PackageSourceFiles', () => {
11
+ test('relativePath writes / separators and no leading ./ from Windows paths', () => {
12
+ jest.isolateModules(() => {
13
+ jest.doMock('path', () => path.win32);
14
+ const { PackageSourceFiles: OnWindows } = require('../src/parser/PackageSourceFiles');
15
+ expect(OnWindows.relativePath('C:\\work\\pkg', 'C:\\work\\pkg\\src\\beta\\deep\\Gamma.ts')).toBe(
16
+ 'src/beta/deep/Gamma.ts'
17
+ );
18
+ expect(OnWindows.relativePath('C:\\work\\pkg\\generated', 'C:\\work\\pkg\\src\\alpha\\Alpha.ts')).toBe(
19
+ '../src/alpha/Alpha.ts'
20
+ );
21
+ expect(OnWindows.relativePath('C:\\work\\pkg\\generated', 'C:\\work\\pkg')).toBe('..');
22
+ });
23
+ });
24
+
25
+ test('relativePath writes the same form from POSIX paths', () => {
26
+ expect(PackageSourceFiles.relativePath('/work/pkg', '/work/pkg/src/beta/deep/Gamma.ts')).toBe(
27
+ 'src/beta/deep/Gamma.ts'
28
+ );
29
+ expect(PackageSourceFiles.relativePath('/work/pkg/generated', '/work/pkg/src/alpha/Alpha.ts')).toBe(
30
+ '../src/alpha/Alpha.ts'
31
+ );
32
+ });
33
+
34
+ test('compare orders by code unit, so upper case sorts before lower case on every machine', () => {
35
+ // 'Z' is 0x5a, 'd' is 0x64: code-unit order puts ZetaPlugin first; a locale collation would not.
36
+ expect(PackageSourceFiles.compare('src/zeta/ZetaPlugin.ts', 'src/zeta/defaults.ts')).toBeLessThan(0);
37
+ expect(PackageSourceFiles.compare('src/zeta/defaults.ts', 'src/zeta/ZetaPlugin.ts')).toBeGreaterThan(0);
38
+ expect(PackageSourceFiles.compare('src/a.ts', 'src/a.ts')).toBe(0);
39
+ // A shorter path sorts before its extension: 'SourceRecord.ts' before 'SourceRecordLoader.ts' ('.' is 0x2e, 'L' is 0x4c).
40
+ expect(PackageSourceFiles.compare('src/SourceRecord.ts', 'src/SourceRecordLoader.ts')).toBeLessThan(0);
41
+ });
42
+ });
@@ -16,6 +16,18 @@ import { writeGeneratedIndex } from '../src/codegen/writeGeneratedIndex';
16
16
  describe('ReflectionDoctor', () => {
17
17
  const fixtureA = path.join(__dirname, 'examples', 'source-repository', 'a');
18
18
 
19
+ const emitFixture = async (pkgDirToBuild: string) => {
20
+ const generatedDir = path.join(pkgDirToBuild, 'generated');
21
+ fs.mkdirSync(generatedDir, { recursive: true });
22
+ const generatedIndexPath = path.join(generatedDir, 'index.ts');
23
+ await writeGeneratedIndex(pkgDirToBuild, generatedDir, generatedIndexPath, ['src']);
24
+ // The doctor reads the built artifact; the sourceGraph/sourceLinks lines it parses are
25
+ // identical between generated/index.ts and its compiled dist twin.
26
+ const distGeneratedDir = path.join(pkgDirToBuild, 'dist', 'generated');
27
+ fs.mkdirSync(distGeneratedDir, { recursive: true });
28
+ fs.copyFileSync(generatedIndexPath, path.join(distGeneratedDir, 'index.js'));
29
+ };
30
+
19
31
  describe('inventory + healthy fixture (fixture a)', () => {
20
32
  it('reports dependency graphs, own graph size, and no orphans', async () => {
21
33
  const doctor = new ReflectionDoctor(fixtureA);
@@ -61,18 +73,6 @@ describe('ReflectionDoctor', () => {
61
73
  describe('broken chains (crafted fixture)', () => {
62
74
  let pkgDir: string;
63
75
 
64
- const emitFixture = async (pkgDirToBuild: string) => {
65
- const generatedDir = path.join(pkgDirToBuild, 'generated');
66
- fs.mkdirSync(generatedDir, { recursive: true });
67
- const generatedIndexPath = path.join(generatedDir, 'index.ts');
68
- await writeGeneratedIndex(pkgDirToBuild, generatedDir, generatedIndexPath, ['src']);
69
- // The doctor reads the built artifact; the sourceGraph/sourceLinks lines it parses are
70
- // identical between generated/index.ts and its compiled dist twin.
71
- const distGeneratedDir = path.join(pkgDirToBuild, 'dist', 'generated');
72
- fs.mkdirSync(distGeneratedDir, { recursive: true });
73
- fs.copyFileSync(generatedIndexPath, path.join(distGeneratedDir, 'index.js'));
74
- };
75
-
76
76
  beforeEach(() => {
77
77
  pkgDir = fs.mkdtempSync(path.join(os.tmpdir(), 'refl-doctor-'));
78
78
  fs.writeFileSync(
@@ -190,4 +190,42 @@ describe('ReflectionDoctor', () => {
190
190
  expect(explanation.verdict).toContain('rebuild');
191
191
  });
192
192
  });
193
+
194
+ describe('a name two files declare (crafted fixture)', () => {
195
+ let pkgDir: string;
196
+
197
+ beforeEach(() => {
198
+ pkgDir = fs.mkdtempSync(path.join(os.tmpdir(), 'refl-doctor-shared-'));
199
+ fs.writeFileSync(
200
+ path.join(pkgDir, 'package.json'),
201
+ JSON.stringify({ name: '@test/doctor-shared-name', version: '1.0.0', dependencies: {} }, null, 2)
202
+ );
203
+ fs.mkdirSync(path.join(pkgDir, 'src', 'alpha'), { recursive: true });
204
+ fs.writeFileSync(path.join(pkgDir, 'src', 'index.ts'), `export * from './alpha/Widget';\n`);
205
+ fs.writeFileSync(
206
+ path.join(pkgDir, 'src', 'alpha', 'Widget.ts'),
207
+ `import { Loadable } from '@proteinjs/reflection';\n\nexport interface Widget extends Loadable {}\n`
208
+ );
209
+ });
210
+
211
+ afterEach(() => {
212
+ fs.rmdirSync(pkgDir, { recursive: true });
213
+ });
214
+
215
+ it('reports a name that reaches the graph from two files, with every declaring file, as the build would refuse it', async () => {
216
+ // Built while one file declared the name; a second file declares it since.
217
+ await emitFixture(pkgDir);
218
+ fs.mkdirSync(path.join(pkgDir, 'src', 'beta'));
219
+ fs.writeFileSync(path.join(pkgDir, 'src', 'beta', 'Widget.ts'), `export class Widget {}\n`);
220
+
221
+ const report = await new ReflectionDoctor(pkgDir).diagnose();
222
+ expect(report.sharedNames).toEqual([
223
+ { qualifiedName: '@test/doctor-shared-name/Widget', filePaths: ['src/alpha/Widget.ts', 'src/beta/Widget.ts'] },
224
+ ]);
225
+ expect(report.healthy).toBe(false);
226
+ expect(report.text).toContain(
227
+ ' @test/doctor-shared-name/Widget\n declared in src/alpha/Widget.ts\n declared in src/beta/Widget.ts'
228
+ );
229
+ });
230
+ });
193
231
  });
@@ -0,0 +1,114 @@
1
+ import * as fs from 'fs';
2
+ import * as path from 'path';
3
+ import { ExamplePackageFixture } from './ExamplePackageFixture';
4
+
5
+ /**
6
+ * The same sources build to the same bytes: the generated index, the compiled dist and the
7
+ * packed tarball do not depend on where the package is checked out, nor on the order the
8
+ * filesystem hands the source files back (which varies by filesystem and from run to run).
9
+ *
10
+ * The enumeration is stubbed at the glob the build reads its sources through: the real glob's
11
+ * result in a fixed base order (code-unit sorted — the glob's own order varies from copy to copy,
12
+ * so a reversal of it would not be the same reversal twice), reordered by `mockEnumerationOrder`.
13
+ */
14
+ let mockEnumerationOrder: (paths: string[]) => string[] = (paths) => paths;
15
+ jest.mock('globby', () => {
16
+ const actual = jest.requireActual('globby');
17
+ const reordered = async (...args: any[]) => mockEnumerationOrder([...(await actual(...args))].sort());
18
+ return Object.assign(reordered, actual);
19
+ });
20
+
21
+ const BUILD_TIMEOUT_MS = 120 * 1000;
22
+
23
+ describe('reproducible generated index', () => {
24
+ const fixture = new ExamplePackageFixture('reproducible', '@proteinjs/reflection-build-test-reproducible');
25
+
26
+ afterEach(() => {
27
+ mockEnumerationOrder = (paths) => paths;
28
+ });
29
+
30
+ afterAll(() => fixture.dispose());
31
+
32
+ test(
33
+ 'two checkouts at different absolute paths ship byte-identical dist/generated/index.js and tarballs',
34
+ async () => {
35
+ const shipped = [];
36
+ for (const location of ['one/pkg', 'two/a/much/deeper/checkout/elsewhere']) {
37
+ const packageDir = fixture.materialize(location);
38
+ await fixture.generate(packageDir);
39
+ fixture.compile(packageDir);
40
+ const generatedJs = fs.readFileSync(path.join(packageDir, 'dist', 'generated', 'index.js'));
41
+ expect(generatedJs.toString()).not.toContain(packageDir);
42
+ shipped.push({ generatedJs: fixture.sha256(generatedJs), tarball: fixture.packSha256(packageDir) });
43
+ }
44
+
45
+ expect(shipped[1]).toEqual(shipped[0]);
46
+ },
47
+ BUILD_TIMEOUT_MS
48
+ );
49
+
50
+ test(
51
+ 'the generated index does not depend on the order the source files are enumerated',
52
+ async () => {
53
+ const orders: { [name: string]: (paths: string[]) => string[] } = {
54
+ asEnumerated: (paths) => paths,
55
+ reversed: (paths) => [...paths].reverse(),
56
+ rotated: (paths) => [...paths.slice(3), ...paths.slice(0, 3)],
57
+ };
58
+ const generated: { [name: string]: string } = {};
59
+ for (const name of Object.keys(orders)) {
60
+ mockEnumerationOrder = orders[name];
61
+ generated[name] = await fixture.generate(fixture.materialize(`order-${name}/pkg`));
62
+ }
63
+
64
+ expect(generated.reversed).toBe(generated.asEnumerated);
65
+ expect(generated.rotated).toBe(generated.asEnumerated);
66
+ },
67
+ BUILD_TIMEOUT_MS
68
+ );
69
+
70
+ test(
71
+ 'node filePaths are package-relative, with / separators and no leading ./',
72
+ async () => {
73
+ const graph = fixture.graphOf(await fixture.generate(fixture.materialize('relative/pkg')));
74
+ const gamma = graph.nodes.find((node) => node.v === fixture.qualified('GammaPlugin'));
75
+ expect(gamma?.value?.filePath).toBe('src/beta/deep/Gamma.ts');
76
+
77
+ const filePaths = graph.nodes.map((node) => node.value?.filePath).filter((filePath) => !!filePath);
78
+ expect(filePaths.length).toBeGreaterThan(0);
79
+ expect(filePaths.filter((filePath) => !/^src\/[A-Za-z]+(\/[A-Za-z]+)*\.ts$/.test(filePath))).toEqual([]);
80
+ },
81
+ BUILD_TIMEOUT_MS
82
+ );
83
+
84
+ test(
85
+ 'nodes are emitted by file then qualified name, edges by child then parent, source links in node order',
86
+ async () => {
87
+ const generated = await fixture.generate(fixture.materialize('canonical/pkg'));
88
+ const graph = fixture.graphOf(generated);
89
+ const byCodeUnit = (keys: string[][]) =>
90
+ [...keys].sort((a, b) => {
91
+ const left = a.join('\0');
92
+ const right = b.join('\0');
93
+ return left < right ? -1 : left > right ? 1 : 0;
94
+ });
95
+
96
+ const nodeKeys = graph.nodes.map((node) => [node.value?.filePath || '', node.v]);
97
+ expect(nodeKeys).toEqual(byCodeUnit(nodeKeys));
98
+ expect(nodeKeys.slice(0, 2)).toEqual([
99
+ ['', '@proteinjs/reflection/Loadable'],
100
+ ['src/alpha/Alpha.ts', fixture.qualified('AardvarkPlugin')],
101
+ ]);
102
+
103
+ const edgeKeys = graph.edges.map((edge) => [edge.v, edge.w]);
104
+ expect(edgeKeys).toEqual(byCodeUnit(edgeKeys));
105
+
106
+ const linkedNames = (generated.match(/^\t'[^']+'/gm) || []).map((line) => line.slice(2, -1));
107
+ const linkableNodeNames = graph.nodes
108
+ .filter((node) => node.value && (node.value.sourceType === 0 || node.value.sourceType === 2))
109
+ .map((node) => node.v);
110
+ expect(linkedNames).toEqual(linkableNodeNames);
111
+ },
112
+ BUILD_TIMEOUT_MS
113
+ );
114
+ });
@@ -0,0 +1,89 @@
1
+ import * as fs from 'fs';
2
+ import * as path from 'path';
3
+ import { ExamplePackageFixture } from './ExamplePackageFixture';
4
+
5
+ /**
6
+ * One qualified name, one declaring file, for every name that reaches the generated index. The
7
+ * index holds one declaration per qualified name, so a name exported by two files would keep only
8
+ * the file parsed last and drop the other without a trace; the build refuses it instead. The rule
9
+ * applies after the prune: a name whose declarations are all pruned (types, consts, interfaces that
10
+ * are not Loadable) never reaches the index and is ordinary TypeScript.
11
+ *
12
+ * The enumeration is stubbed at the glob, as in the reproducible suite: the real glob's result in a
13
+ * fixed base order (code-unit sorted), reordered by `mockEnumerationOrder`.
14
+ */
15
+ let mockEnumerationOrder: (paths: string[]) => string[] = (paths) => paths;
16
+ jest.mock('globby', () => {
17
+ const actual = jest.requireActual('globby');
18
+ const reordered = async (...args: any[]) => mockEnumerationOrder([...(await actual(...args))].sort());
19
+ return Object.assign(reordered, actual);
20
+ });
21
+
22
+ const BUILD_TIMEOUT_MS = 120 * 1000;
23
+
24
+ /** What one build did: the refusal's message, or the file whose declaration `name` holds in the index it wrote. */
25
+ const outcomeOf = (fixture: ExamplePackageFixture, packageDir: string, name: string) =>
26
+ fixture.generate(packageDir).then(
27
+ (generated) => ({
28
+ built:
29
+ fixture.graphOf(generated).nodes.find((node) => node.v === fixture.qualified(name))?.value?.filePath || null,
30
+ }),
31
+ (error: Error) => ({ refused: error.message })
32
+ );
33
+
34
+ describe('a name exported by two files', () => {
35
+ const sharedName = new ExamplePackageFixture('shared-name', '@proteinjs/reflection-build-test-shared-name');
36
+ const prunedPair = new ExamplePackageFixture(
37
+ 'shared-name-pruned',
38
+ '@proteinjs/reflection-build-test-shared-name-pruned'
39
+ );
40
+ // Widget is declared by two files. Gear (src/gamma/Gear.ts) is declared twice by ONE file, an
41
+ // interface and a const, and reaches the index too: it is not a shared name, so it is not named.
42
+ const refusal = [
43
+ 'reflection-build refused @proteinjs/reflection-build-test-shared-name: a name that reaches the generated index is declared in more than one file.',
44
+ 'The index holds one declaration per qualified name, so the file parsed last would replace the others without a trace.',
45
+ 'Give each declaration its own name (no index was written):',
46
+ ' @proteinjs/reflection-build-test-shared-name/Widget: src/alpha/Widget.ts, src/beta/Widget.ts',
47
+ ].join('\n');
48
+
49
+ afterEach(() => {
50
+ mockEnumerationOrder = (paths) => paths;
51
+ });
52
+
53
+ afterAll(() => {
54
+ sharedName.dispose();
55
+ prunedPair.dispose();
56
+ });
57
+
58
+ test(
59
+ 'reaching the index (an interface that extends Loadable, a class): the build refuses, naming the package, the name and both files, and writes no index',
60
+ async () => {
61
+ const packageDir = sharedName.materialize('refused/pkg');
62
+ expect(await outcomeOf(sharedName, packageDir, 'Widget')).toEqual({ refused: refusal });
63
+ expect(fs.existsSync(path.join(packageDir, 'generated', 'index.ts'))).toBe(false);
64
+ },
65
+ BUILD_TIMEOUT_MS
66
+ );
67
+
68
+ test(
69
+ 'the refusal names the files in package-relative path order, whatever order they are enumerated in',
70
+ async () => {
71
+ mockEnumerationOrder = (paths) => [...paths].reverse();
72
+ expect(await outcomeOf(sharedName, sharedName.materialize('reversed/pkg'), 'Widget')).toEqual({
73
+ refused: refusal,
74
+ });
75
+ },
76
+ BUILD_TIMEOUT_MS
77
+ );
78
+
79
+ test(
80
+ 'pruned before the index (a type and an interface; two consts): the build writes the index, without the names',
81
+ async () => {
82
+ const packageDir = prunedPair.materialize('built/pkg');
83
+ expect(await outcomeOf(prunedPair, packageDir, 'Shape')).toEqual({ built: null });
84
+ expect(await outcomeOf(prunedPair, packageDir, 'LIMITS')).toEqual({ built: null });
85
+ expect(await outcomeOf(prunedPair, packageDir, 'SquareTile')).toEqual({ built: 'src/alpha/Tile.ts' });
86
+ },
87
+ BUILD_TIMEOUT_MS
88
+ );
89
+ });
@@ -0,0 +1,4 @@
1
+ export * from './src/alpha/Alpha';
2
+ export * from './src/beta/Beta';
3
+ export * from './src/beta/deep/Gamma';
4
+ export * from './src/zeta/Zeta';
@@ -0,0 +1,13 @@
1
+ import { Loadable } from '@proteinjs/reflection';
2
+
3
+ export interface Plugin extends Loadable {
4
+ id: string;
5
+ }
6
+
7
+ export class AlphaPlugin implements Plugin {
8
+ id = 'alpha';
9
+ }
10
+
11
+ export class AardvarkPlugin implements Plugin {
12
+ id = 'aardvark';
13
+ }
@@ -0,0 +1,4 @@
1
+ import { Plugin } from './Alpha';
2
+
3
+ // The same file name as src/zeta/Defaults.ts, a different exported name: one declaration per qualified name.
4
+ export const defaults: Plugin = { id: 'alpha-defaults' };
@@ -0,0 +1,7 @@
1
+ import { Plugin } from '../alpha/Alpha';
2
+
3
+ export class BetaPlugin implements Plugin {
4
+ id = 'beta';
5
+ }
6
+
7
+ export const betaSetting: Plugin = { id: 'beta-setting' };
@@ -0,0 +1,5 @@
1
+ import { Plugin } from '../../alpha/Alpha';
2
+
3
+ export class GammaPlugin implements Plugin {
4
+ id = 'gamma';
5
+ }
@@ -0,0 +1,4 @@
1
+ import { Plugin } from '../alpha/Alpha';
2
+
3
+ // The same file name as src/alpha/Defaults.ts, a different exported name: one declaration per qualified name.
4
+ export const zetaDefaults: Plugin = { id: 'zeta-defaults' };
@@ -0,0 +1,9 @@
1
+ import { Plugin } from '../alpha/Alpha';
2
+
3
+ export class ZetaPlugin implements Plugin {
4
+ id = 'zeta';
5
+ }
6
+
7
+ export class MuPlugin implements Plugin {
8
+ id = 'mu';
9
+ }
@@ -0,0 +1 @@
1
+ export * from './src/alpha/Widget';
@@ -0,0 +1,6 @@
1
+ import { Loadable } from '@proteinjs/reflection';
2
+
3
+ // Same exported name as src/beta/Widget.ts: the declaration contract.
4
+ export interface Widget extends Loadable {
5
+ id: string;
6
+ }
@@ -0,0 +1,5 @@
1
+ // Same exported name as src/alpha/Widget.ts: a plain class. One qualified name, two declarations,
2
+ // and the name reaches the generated index (the interface is Loadable).
3
+ export class Widget {
4
+ id = 'beta';
5
+ }
@@ -0,0 +1,9 @@
1
+ import { Loadable } from '@proteinjs/reflection';
2
+
3
+ // One file declares Gear twice, in TypeScript's type and value spaces: one declaring file, so the
4
+ // name is not shared, though it reaches the index. Only a name declared by two FILES is refused.
5
+ export interface Gear extends Loadable {
6
+ teeth: number;
7
+ }
8
+
9
+ export const Gear = { teeth: 12 };
@@ -0,0 +1 @@
1
+ export * from './src/alpha/Tile';
@@ -0,0 +1,2 @@
1
+ // Same exported name as src/beta/Limits.ts: two consts, neither Loadable; pruned, never in the index.
2
+ export const LIMITS = { max: 3 };
@@ -0,0 +1,3 @@
1
+ // Same exported name as src/beta/Shape.ts. Neither declaration is Loadable, so the build prunes the
2
+ // name and it never reaches the generated index: ordinary TypeScript, not a shared name to refuse.
3
+ export type Shape = { sides: number };
@@ -0,0 +1,9 @@
1
+ import { Loadable } from '@proteinjs/reflection';
2
+
3
+ export interface Tile extends Loadable {
4
+ id: string;
5
+ }
6
+
7
+ export class SquareTile implements Tile {
8
+ id = 'square';
9
+ }
@@ -0,0 +1,2 @@
1
+ // Same exported name as src/alpha/Limits.ts: two consts, neither Loadable; pruned, never in the index.
2
+ export const LIMITS = { max: 5 };