@ontrails/adapter-kit 1.0.0-beta.39 → 1.0.0-beta.42

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ontrails/adapter-kit",
3
- "version": "1.0.0-beta.39",
3
+ "version": "1.0.0-beta.42",
4
4
  "description": "Internal adapter authoring kit for Trails metadata, scaffolding, and checks.",
5
5
  "files": [
6
6
  "src/**/*.ts",
@@ -21,7 +21,8 @@
21
21
  "clean": "rm -rf dist *.tsbuildinfo"
22
22
  },
23
23
  "dependencies": {
24
- "@ontrails/core": "^1.0.0-beta.39"
24
+ "@ontrails/core": "^1.0.0-beta.42",
25
+ "@ontrails/source": "^1.0.0-beta.42"
25
26
  },
26
27
  "peerDependencies": {
27
28
  "zod": "^4.3.5"
package/src/catalog.ts CHANGED
@@ -6,11 +6,17 @@
6
6
  * internal tooling package.
7
7
  */
8
8
 
9
- import { existsSync, readFileSync, realpathSync, statSync } from 'node:fs';
10
- import { dirname, join, resolve } from 'node:path';
9
+ import { existsSync, realpathSync, statSync } from 'node:fs';
10
+ import { join, resolve } from 'node:path';
11
11
 
12
12
  import { listWorkspacePackages } from '@ontrails/core';
13
13
 
14
+ import {
15
+ adapterSourceExportKind,
16
+ adapterSourceExportKindHasType,
17
+ adapterSourceExportKindHasValue,
18
+ } from './source.js';
19
+
14
20
  export const adapterTargetPlacements = ['extracted', 'subpath'] as const;
15
21
 
16
22
  export type AdapterTargetPlacementValue =
@@ -85,47 +91,12 @@ interface ParsedCatalogTarget {
85
91
  readonly targetEntry?: AdapterTargetCatalogEntry | undefined;
86
92
  }
87
93
 
88
- type ExportKind = 'type' | 'type-value' | 'value';
89
-
90
- interface StarExportSpecifier {
91
- readonly specifier: string;
92
- readonly typeOnly: boolean;
93
- }
94
-
95
- interface NamedExportSpecifier {
96
- readonly identifier: string;
97
- readonly specifier: string;
98
- readonly typeOnly: boolean;
99
- }
100
-
101
- interface NamedImportSpecifier {
102
- readonly identifier: string;
103
- readonly specifier: string;
104
- readonly typeOnly: boolean;
105
- }
106
-
107
- interface ExportListSpecifier {
108
- readonly exported: string;
109
- readonly local: string;
110
- readonly typeOnly: boolean;
111
- }
112
-
113
- type ImportKindResolver = (
114
- importSpecifier: NamedImportSpecifier
115
- ) => ExportKind | undefined;
116
-
117
94
  const isRecord = (value: unknown): value is Record<string, unknown> =>
118
95
  Boolean(value && typeof value === 'object' && !Array.isArray(value));
119
96
 
120
97
  const targetIdPattern = /^[a-z][a-z0-9-]*$/u;
121
98
  const exportIdentifierPattern = /^[A-Za-z_$][\w$]*$/u;
122
99
 
123
- const exportKindHasType = (kind: ExportKind | undefined): boolean =>
124
- kind === 'type' || kind === 'type-value';
125
-
126
- const exportKindHasValue = (kind: ExportKind | undefined): boolean =>
127
- kind === 'value' || kind === 'type-value';
128
-
129
100
  const normalizePath = (path: string): string => path.replaceAll('\\', '/');
130
101
 
131
102
  const normalizeRealPath = (path: string): string => {
@@ -144,264 +115,6 @@ const pathIsFile = (path: string): boolean => {
144
115
  }
145
116
  };
146
117
 
147
- const localDeclarationKind = (
148
- code: string,
149
- identifier: string,
150
- exportedOnly = false
151
- ): ExportKind | undefined => {
152
- const escapedIdentifier = identifier.replaceAll(
153
- /[.*+?^${}()|[\]\\]/gu,
154
- '\\$&'
155
- );
156
- const exportPrefix = exportedOnly ? '\\bexport\\s+' : '\\b(?:export\\s+)?';
157
- const valueDeclarationPattern = new RegExp(
158
- `${exportPrefix}(?!declare\\s+)(?:(?:async\\s+)?function|const|let|var)\\s+${escapedIdentifier}\\b`,
159
- 'u'
160
- );
161
- if (valueDeclarationPattern.test(code)) {
162
- return 'value';
163
- }
164
-
165
- const typeValueDeclarationPattern = new RegExp(
166
- `${exportPrefix}(?!declare\\s+)(?:abstract\\s+)?(?:class|enum)\\s+${escapedIdentifier}\\b`,
167
- 'u'
168
- );
169
- if (typeValueDeclarationPattern.test(code)) {
170
- return 'type-value';
171
- }
172
-
173
- const typeDeclarationPattern = new RegExp(
174
- `${exportPrefix}(?:declare\\s+)?(?:interface|type)\\s+${escapedIdentifier}\\b`,
175
- 'u'
176
- );
177
- return typeDeclarationPattern.test(code) ? 'type' : undefined;
178
- };
179
-
180
- const maskDeadSourceText = (
181
- source: string,
182
- options: { strings: boolean }
183
- ): string => {
184
- const output = [...source];
185
- let index = 0;
186
-
187
- const maskRange = (start: number, end: number): void => {
188
- for (let cursor = start; cursor < end; cursor += 1) {
189
- if (output[cursor] !== '\n') {
190
- output[cursor] = ' ';
191
- }
192
- }
193
- };
194
-
195
- const skipQuoted = (quote: '"' | "'" | '`'): void => {
196
- const start = index;
197
- index += 1;
198
- while (index < source.length) {
199
- if (source[index] === '\\') {
200
- index += 2;
201
- continue;
202
- }
203
- if (source[index] === quote) {
204
- index += 1;
205
- break;
206
- }
207
- index += 1;
208
- }
209
- if (options.strings) {
210
- maskRange(start, index);
211
- }
212
- };
213
-
214
- while (index < source.length) {
215
- if (source.startsWith('//', index)) {
216
- const end = source.indexOf('\n', index + 2);
217
- const stop = end === -1 ? source.length : end;
218
- maskRange(index, stop);
219
- index = stop;
220
- continue;
221
- }
222
- if (source.startsWith('/*', index)) {
223
- const end = source.indexOf('*/', index + 2);
224
- const stop = end === -1 ? source.length : end + 2;
225
- maskRange(index, stop);
226
- index = stop;
227
- continue;
228
- }
229
- const char = source[index];
230
- if (char === '"' || char === "'" || char === '`') {
231
- skipQuoted(char);
232
- continue;
233
- }
234
- index += 1;
235
- }
236
-
237
- return output.join('');
238
- };
239
-
240
- const defaultExportKind = (source: string): ExportKind | undefined => {
241
- const code = maskDeadSourceText(source, { strings: true });
242
- if (/\bexport\s+default\s+(?:async\s+)?function\*?\b/u.test(code)) {
243
- return 'value';
244
- }
245
- if (/\bexport\s+default\s+(?:abstract\s+)?(?:class|enum)\b/u.test(code)) {
246
- return 'type-value';
247
- }
248
- if (/\bexport\s+default\s+(?:interface|type)\b/u.test(code)) {
249
- return 'type';
250
- }
251
-
252
- const expressionIdentifier =
253
- /\bexport\s+default\s+(?<identifier>[A-Za-z_$][\w$]*)\b/u.exec(code)
254
- ?.groups?.['identifier'];
255
- if (expressionIdentifier) {
256
- return localDeclarationKind(code, expressionIdentifier) ?? 'value';
257
- }
258
-
259
- return /\bexport\s+default\b/u.test(code) ? 'value' : undefined;
260
- };
261
-
262
- const localDefaultImportSpecifier = (
263
- code: string,
264
- stringsMaskedCode: string,
265
- identifier: string
266
- ): NamedImportSpecifier | undefined => {
267
- const pattern =
268
- /\bimport\s+(?<importTypeOnly>type\s+)?(?<local>[A-Za-z_$][\w$]*)(?:\s*,\s*(?:\{[\s\S]*?\}|\*\s+as\s+[A-Za-z_$][\w$]*))?\s+from\s+['"](?<specifier>[^'"]+)['"]/gu;
269
-
270
- for (const match of code.matchAll(pattern)) {
271
- if (!stringsMaskedCode.startsWith('import', match.index ?? 0)) {
272
- continue;
273
- }
274
- if (match.groups?.['local'] !== identifier) {
275
- continue;
276
- }
277
-
278
- return {
279
- identifier: 'default',
280
- specifier: match.groups?.['specifier'] ?? '',
281
- typeOnly: Boolean(match.groups?.['importTypeOnly']),
282
- };
283
- }
284
-
285
- return undefined;
286
- };
287
-
288
- const parseNamedImportSpecifier = (
289
- item: string,
290
- declarationTypeOnly: boolean,
291
- specifier: string,
292
- identifier: string
293
- ): NamedImportSpecifier | undefined => {
294
- const trimmedItem = item.trim();
295
- if (!trimmedItem) {
296
- return undefined;
297
- }
298
-
299
- const itemTypeOnly = declarationTypeOnly || trimmedItem.startsWith('type ');
300
- const specifierText = trimmedItem.replace(/^type\s+/u, '');
301
- const imported =
302
- /^(?<imported>[A-Za-z_$][\w$]*)(?:\s+as\s+(?<local>[A-Za-z_$][\w$]*))?$/u.exec(
303
- specifierText
304
- )?.groups;
305
- const local = imported?.['local'] ?? imported?.['imported'];
306
- const importedName = imported?.['imported'];
307
- if (local !== identifier || !importedName) {
308
- return undefined;
309
- }
310
-
311
- return {
312
- identifier: importedName,
313
- specifier,
314
- typeOnly: itemTypeOnly,
315
- };
316
- };
317
-
318
- const localNamedImportSpecifier = (
319
- source: string,
320
- identifier: string
321
- ): NamedImportSpecifier | undefined => {
322
- const code = maskDeadSourceText(source, { strings: false });
323
- const stringsMaskedCode = maskDeadSourceText(source, { strings: true });
324
- const defaultSpecifier = localDefaultImportSpecifier(
325
- code,
326
- stringsMaskedCode,
327
- identifier
328
- );
329
- if (defaultSpecifier) {
330
- return defaultSpecifier;
331
- }
332
-
333
- const pattern =
334
- /\bimport\s+(?<importTypeOnly>type\s+)?\{(?<imports>[\s\S]*?)\}\s+from\s+['"](?<specifier>[^'"]+)['"]/gu;
335
-
336
- for (const match of code.matchAll(pattern)) {
337
- if (!stringsMaskedCode.startsWith('import', match.index ?? 0)) {
338
- continue;
339
- }
340
-
341
- const declarationTypeOnly = Boolean(match.groups?.['importTypeOnly']);
342
- const namedImports = match.groups?.['imports'] ?? '';
343
- const specifier = match.groups?.['specifier'] ?? '';
344
- for (const item of namedImports.split(',')) {
345
- const namedSpecifier = parseNamedImportSpecifier(
346
- item,
347
- declarationTypeOnly,
348
- specifier,
349
- identifier
350
- );
351
- if (namedSpecifier) {
352
- return namedSpecifier;
353
- }
354
- }
355
- }
356
-
357
- return undefined;
358
- };
359
-
360
- const parseExportListSpecifier = (
361
- item: string,
362
- declarationTypeOnly: boolean
363
- ): ExportListSpecifier | undefined => {
364
- const trimmedItem = item.trim();
365
- if (!trimmedItem) {
366
- return undefined;
367
- }
368
-
369
- const typeOnly = declarationTypeOnly || trimmedItem.startsWith('type ');
370
- const specifierText = trimmedItem.replace(/^type\s+/u, '');
371
- const exported =
372
- /^(?<local>[A-Za-z_$][\w$]*)(?:\s+as\s+(?<name>[A-Za-z_$][\w$]*))?$/u.exec(
373
- specifierText
374
- )?.groups;
375
- const local = exported?.['local'];
376
- if (!local) {
377
- return undefined;
378
- }
379
-
380
- return {
381
- exported: exported?.['name'] ?? local,
382
- local,
383
- typeOnly,
384
- };
385
- };
386
-
387
- const exportedLocalBindingKind = (
388
- source: string,
389
- code: string,
390
- local: string,
391
- resolveImportKind: ImportKindResolver
392
- ): ExportKind | undefined => {
393
- const localKind = localDeclarationKind(code, local);
394
- if (localKind) {
395
- return localKind;
396
- }
397
-
398
- const importSpecifier = localNamedImportSpecifier(source, local);
399
- if (importSpecifier?.typeOnly) {
400
- return 'type';
401
- }
402
- return importSpecifier ? resolveImportKind(importSpecifier) : undefined;
403
- };
404
-
405
118
  const exportConditions = new Set([
406
119
  'bun',
407
120
  'node',
@@ -870,228 +583,6 @@ const normalizeConformance = (
870
583
  };
871
584
  };
872
585
 
873
- const namedExportKind = (
874
- source: string,
875
- identifier: string,
876
- resolveImportKind: ImportKindResolver
877
- ): ExportKind | undefined => {
878
- if (identifier === 'default') {
879
- const defaultKind = defaultExportKind(source);
880
- if (defaultKind) {
881
- return defaultKind;
882
- }
883
- }
884
-
885
- const code = maskDeadSourceText(source, { strings: true });
886
- const directKind = localDeclarationKind(code, identifier, true);
887
- if (directKind) {
888
- return directKind;
889
- }
890
-
891
- const exportListPattern =
892
- /\bexport\s+(?<typeOnly>type\s+)?\{(?<exports>[\s\S]*?)\}(?!\s+from\b)/gu;
893
-
894
- for (const match of code.matchAll(exportListPattern)) {
895
- const declarationTypeOnly = Boolean(match.groups?.['typeOnly']);
896
- const namedExports = match.groups?.['exports'] ?? '';
897
- for (const item of namedExports.split(',')) {
898
- const exported = parseExportListSpecifier(item, declarationTypeOnly);
899
- if (exported?.exported !== identifier) {
900
- continue;
901
- }
902
-
903
- const localKind = exportedLocalBindingKind(
904
- source,
905
- code,
906
- exported.local,
907
- resolveImportKind
908
- );
909
- if (!localKind) {
910
- return undefined;
911
- }
912
- return exported.typeOnly ? 'type' : localKind;
913
- }
914
- }
915
-
916
- return undefined;
917
- };
918
-
919
- const starExportSpecifiers = (
920
- source: string
921
- ): readonly StarExportSpecifier[] => {
922
- const code = maskDeadSourceText(source, { strings: false });
923
- const stringsMaskedCode = maskDeadSourceText(source, { strings: true });
924
- return [
925
- ...code.matchAll(
926
- /\bexport\s+(?<typeOnly>type\s+)?\*\s+from\s+['"](?<specifier>[^'"]+)['"]/gu
927
- ),
928
- ]
929
- .filter((match) => stringsMaskedCode.startsWith('export', match.index ?? 0))
930
- .map((match) => ({
931
- specifier: match.groups?.['specifier'] ?? '',
932
- typeOnly: Boolean(match.groups?.['typeOnly']),
933
- }));
934
- };
935
-
936
- const namedExportSpecifiers = (
937
- source: string,
938
- identifier: string
939
- ): readonly NamedExportSpecifier[] => {
940
- const code = maskDeadSourceText(source, { strings: false });
941
- const stringsMaskedCode = maskDeadSourceText(source, { strings: true });
942
- const exports: NamedExportSpecifier[] = [];
943
- const pattern =
944
- /\bexport\s+(?<typeOnly>type\s+)?\{(?<exports>[\s\S]*?)\}\s+from\s+['"](?<specifier>[^'"]+)['"]/gu;
945
-
946
- for (const match of code.matchAll(pattern)) {
947
- if (!stringsMaskedCode.startsWith('export', match.index ?? 0)) {
948
- continue;
949
- }
950
-
951
- const specifier = match.groups?.['specifier'];
952
- if (!specifier?.startsWith('.')) {
953
- continue;
954
- }
955
-
956
- const declarationTypeOnly = Boolean(match.groups?.['typeOnly']);
957
- const namedExports = match.groups?.['exports'] ?? '';
958
- for (const item of namedExports.split(',')) {
959
- const trimmedItem = item.trim();
960
- if (!trimmedItem) {
961
- continue;
962
- }
963
-
964
- const itemTypeOnly =
965
- declarationTypeOnly || trimmedItem.startsWith('type ');
966
- const specifierText = trimmedItem.replace(/^type\s+/u, '');
967
- const exported =
968
- /^(?<local>[A-Za-z_$][\w$]*)(?:\s+as\s+(?<name>[A-Za-z_$][\w$]*))?$/u.exec(
969
- specifierText
970
- )?.groups;
971
- const local = exported?.['local'];
972
- if (!local || (exported?.['name'] ?? local) !== identifier) {
973
- continue;
974
- }
975
-
976
- exports.push({
977
- identifier: local,
978
- specifier,
979
- typeOnly: itemTypeOnly,
980
- });
981
- }
982
- }
983
-
984
- return exports;
985
- };
986
-
987
- const resolveLocalModuleSpecifier = (
988
- sourcePath: string,
989
- specifier: string
990
- ): string | undefined => {
991
- if (!specifier.startsWith('.')) {
992
- return undefined;
993
- }
994
-
995
- const basePath = resolve(dirname(sourcePath), specifier);
996
- const candidates = [
997
- basePath,
998
- basePath.endsWith('.js') ? `${basePath.slice(0, -3)}.ts` : undefined,
999
- basePath.endsWith('.js') ? `${basePath.slice(0, -3)}.tsx` : undefined,
1000
- basePath.endsWith('.mjs') ? `${basePath.slice(0, -4)}.mts` : undefined,
1001
- `${basePath}.ts`,
1002
- `${basePath}.tsx`,
1003
- join(basePath, 'index.ts'),
1004
- join(basePath, 'index.tsx'),
1005
- ].filter((candidate): candidate is string => candidate !== undefined);
1006
-
1007
- return candidates.find((candidate) => existsSync(candidate));
1008
- };
1009
-
1010
- const namedExportKindFromFile = (
1011
- sourcePath: string,
1012
- identifier: string,
1013
- visited = new Set<string>()
1014
- ): ExportKind | undefined => {
1015
- const normalizedSourcePath = normalizeRealPath(sourcePath);
1016
- const visitKey = `${normalizedSourcePath}:${identifier}`;
1017
- if (visited.has(visitKey)) {
1018
- return undefined;
1019
- }
1020
- visited.add(visitKey);
1021
-
1022
- let source: string;
1023
- try {
1024
- source = readFileSync(normalizedSourcePath, 'utf8');
1025
- } catch {
1026
- return undefined;
1027
- }
1028
-
1029
- const directKind = namedExportKind(source, identifier, (importSpecifier) => {
1030
- const importTarget = resolveLocalModuleSpecifier(
1031
- normalizedSourcePath,
1032
- importSpecifier.specifier
1033
- );
1034
- if (!importTarget) {
1035
- return;
1036
- }
1037
- return namedExportKindFromFile(
1038
- importTarget,
1039
- importSpecifier.identifier,
1040
- new Set(visited)
1041
- );
1042
- });
1043
- if (directKind) {
1044
- return directKind;
1045
- }
1046
-
1047
- let sawTypeExport = false;
1048
- for (const exportSpecifier of namedExportSpecifiers(source, identifier)) {
1049
- const exportTarget = resolveLocalModuleSpecifier(
1050
- normalizedSourcePath,
1051
- exportSpecifier.specifier
1052
- );
1053
- if (!exportTarget) {
1054
- continue;
1055
- }
1056
-
1057
- const reexportedKind = namedExportKindFromFile(
1058
- exportTarget,
1059
- exportSpecifier.identifier,
1060
- new Set(visited)
1061
- );
1062
- if (exportKindHasValue(reexportedKind) && !exportSpecifier.typeOnly) {
1063
- return reexportedKind;
1064
- }
1065
- if (exportKindHasType(reexportedKind)) {
1066
- sawTypeExport = true;
1067
- }
1068
- }
1069
-
1070
- for (const exportSpecifier of starExportSpecifiers(source)) {
1071
- const exportTarget = resolveLocalModuleSpecifier(
1072
- normalizedSourcePath,
1073
- exportSpecifier.specifier
1074
- );
1075
- if (!exportTarget) {
1076
- continue;
1077
- }
1078
-
1079
- const reexportedKind = namedExportKindFromFile(
1080
- exportTarget,
1081
- identifier,
1082
- new Set(visited)
1083
- );
1084
- if (exportKindHasValue(reexportedKind) && !exportSpecifier.typeOnly) {
1085
- return reexportedKind;
1086
- }
1087
- if (exportKindHasType(reexportedKind)) {
1088
- sawTypeExport = true;
1089
- }
1090
- }
1091
-
1092
- return sawTypeExport ? 'type' : undefined;
1093
- };
1094
-
1095
586
  const conformanceExportDiagnostics = (
1096
587
  context: AdapterTargetParseContext,
1097
588
  target: string,
@@ -1114,11 +605,14 @@ const conformanceExportDiagnostics = (
1114
605
  keyof AdapterTargetConformanceManifest,
1115
606
  string,
1116
607
  ][]) {
1117
- const exportKind = namedExportKindFromFile(testingExportTarget, identifier);
1118
- if (field === 'adapterType' && exportKindHasType(exportKind)) {
608
+ const exportKind = adapterSourceExportKind(testingExportTarget, identifier);
609
+ if (field === 'adapterType' && adapterSourceExportKindHasType(exportKind)) {
1119
610
  continue;
1120
611
  }
1121
- if (field !== 'adapterType' && exportKindHasValue(exportKind)) {
612
+ if (
613
+ field !== 'adapterType' &&
614
+ adapterSourceExportKindHasValue(exportKind)
615
+ ) {
1122
616
  continue;
1123
617
  }
1124
618
  const expectedExport =