@expo-harmony/expo-modules-autolinking 55.0.25-harmony.1

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 (103) hide show
  1. package/README.md +12 -0
  2. package/bin/expo-harmony-autolinking.ts +14 -0
  3. package/build/bin/expo-harmony-autolinking.d.ts +2 -0
  4. package/build/bin/expo-harmony-autolinking.js +11 -0
  5. package/build/index.d.ts +8 -0
  6. package/build/index.js +15 -0
  7. package/build/src/autolinking/link.d.ts +3 -0
  8. package/build/src/autolinking/link.js +150 -0
  9. package/build/src/autolinking/resolve.d.ts +4 -0
  10. package/build/src/autolinking/resolve.js +25 -0
  11. package/build/src/autolinking/search.d.ts +3 -0
  12. package/build/src/autolinking/search.js +357 -0
  13. package/build/src/autolinking/verify.d.ts +4 -0
  14. package/build/src/autolinking/verify.js +366 -0
  15. package/build/src/commands/autolinkingOptions.d.ts +12 -0
  16. package/build/src/commands/autolinkingOptions.js +35 -0
  17. package/build/src/commands/cli.d.ts +2 -0
  18. package/build/src/commands/cli.js +50 -0
  19. package/build/src/commands/link.d.ts +2 -0
  20. package/build/src/commands/link.js +27 -0
  21. package/build/src/commands/output.d.ts +3 -0
  22. package/build/src/commands/output.js +33 -0
  23. package/build/src/commands/resolve.d.ts +2 -0
  24. package/build/src/commands/resolve.js +13 -0
  25. package/build/src/commands/search.d.ts +2 -0
  26. package/build/src/commands/search.js +13 -0
  27. package/build/src/commands/verify.d.ts +2 -0
  28. package/build/src/commands/verify.js +17 -0
  29. package/build/src/config/constants.d.ts +17 -0
  30. package/build/src/config/constants.js +40 -0
  31. package/build/src/config/options.d.ts +13 -0
  32. package/build/src/config/options.js +202 -0
  33. package/build/src/errors.d.ts +18 -0
  34. package/build/src/errors.js +35 -0
  35. package/build/src/harmony/manifest/generate.d.ts +4 -0
  36. package/build/src/harmony/manifest/generate.js +55 -0
  37. package/build/src/harmony/manifest/index.d.ts +7 -0
  38. package/build/src/harmony/manifest/index.js +64 -0
  39. package/build/src/harmony/materialize.d.ts +12 -0
  40. package/build/src/harmony/materialize.js +278 -0
  41. package/build/src/harmony/ohpm/dependencies.d.ts +7 -0
  42. package/build/src/harmony/ohpm/dependencies.js +52 -0
  43. package/build/src/harmony/persistence/canonicalize.d.ts +15 -0
  44. package/build/src/harmony/persistence/canonicalize.js +108 -0
  45. package/build/src/harmony/providers/host.d.ts +3 -0
  46. package/build/src/harmony/providers/host.js +88 -0
  47. package/build/src/harmony/rnoh/cli.d.ts +2 -0
  48. package/build/src/harmony/rnoh/cli.js +55 -0
  49. package/build/src/harmony/rnoh/link.d.ts +2 -0
  50. package/build/src/harmony/rnoh/link.js +419 -0
  51. package/build/src/harmony/rnoh/packageMetadata.d.ts +12 -0
  52. package/build/src/harmony/rnoh/packageMetadata.js +125 -0
  53. package/build/src/harmony/transaction/previousState.d.ts +15 -0
  54. package/build/src/harmony/transaction/previousState.js +96 -0
  55. package/build/src/harmony/transaction/publish.d.ts +6 -0
  56. package/build/src/harmony/transaction/publish.js +272 -0
  57. package/build/src/harmony/transaction/stageProject.d.ts +18 -0
  58. package/build/src/harmony/transaction/stageProject.js +313 -0
  59. package/build/src/metadata/descriptor.d.ts +19 -0
  60. package/build/src/metadata/descriptor.js +306 -0
  61. package/build/src/metadata/host.d.ts +5 -0
  62. package/build/src/metadata/host.js +37 -0
  63. package/build/src/metadata/schema.d.ts +10 -0
  64. package/build/src/metadata/schema.js +171 -0
  65. package/build/src/types.d.ts +194 -0
  66. package/build/src/types.js +2 -0
  67. package/build/src/utilities/values.d.ts +17 -0
  68. package/build/src/utilities/values.js +205 -0
  69. package/harmony-module.schema.json +28 -0
  70. package/index.ts +41 -0
  71. package/package.json +66 -0
  72. package/src/autolinking/link.ts +170 -0
  73. package/src/autolinking/resolve.ts +33 -0
  74. package/src/autolinking/search.ts +369 -0
  75. package/src/autolinking/verify.ts +415 -0
  76. package/src/commands/autolinkingOptions.ts +38 -0
  77. package/src/commands/cli.ts +56 -0
  78. package/src/commands/link.ts +28 -0
  79. package/src/commands/output.ts +36 -0
  80. package/src/commands/resolve.ts +14 -0
  81. package/src/commands/search.ts +14 -0
  82. package/src/commands/verify.ts +17 -0
  83. package/src/config/constants.ts +45 -0
  84. package/src/config/options.ts +195 -0
  85. package/src/errors.ts +48 -0
  86. package/src/harmony/manifest/generate.ts +65 -0
  87. package/src/harmony/manifest/index.ts +81 -0
  88. package/src/harmony/materialize.ts +382 -0
  89. package/src/harmony/ohpm/dependencies.ts +59 -0
  90. package/src/harmony/persistence/canonicalize.ts +156 -0
  91. package/src/harmony/providers/host.ts +147 -0
  92. package/src/harmony/rnoh/cli.ts +59 -0
  93. package/src/harmony/rnoh/link.ts +481 -0
  94. package/src/harmony/rnoh/packageMetadata.ts +105 -0
  95. package/src/harmony/transaction/previousState.ts +71 -0
  96. package/src/harmony/transaction/publish.ts +287 -0
  97. package/src/harmony/transaction/stageProject.ts +345 -0
  98. package/src/metadata/descriptor.ts +324 -0
  99. package/src/metadata/host.ts +41 -0
  100. package/src/metadata/schema.ts +184 -0
  101. package/src/types.ts +220 -0
  102. package/src/utilities/values.ts +190 -0
  103. package/tsconfig.json +24 -0
@@ -0,0 +1,41 @@
1
+ import { HarmonyAutolinkingError } from '../errors';
2
+ import { sortedUniqueStrings } from '../utilities/values';
3
+
4
+ const ArkTsIdentifierPattern = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
5
+ const HostMetadataFields = Object.freeze([
6
+ 'rootViewComponents',
7
+ ]);
8
+
9
+ function normalizeIdentifiers(value, field, packageName) {
10
+ if (value === undefined) return [];
11
+
12
+ if (!Array.isArray(value)) {
13
+ throw new HarmonyAutolinkingError('INVALID_METADATA', `expo-module.config.json#harmony.${field} must be an array.`, {
14
+ packageName,
15
+ stage: 'metadata',
16
+ });
17
+ }
18
+ const values = value.map((item, index) => {
19
+ if (typeof item !== 'string' || !ArkTsIdentifierPattern.test(item)) {
20
+ throw new HarmonyAutolinkingError('INVALID_METADATA', `harmony.${field}[${index}] must be a valid ArkTS identifier.`, {
21
+ packageName,
22
+ stage: 'metadata',
23
+ });
24
+ }
25
+ return item;
26
+ });
27
+
28
+ return sortedUniqueStrings(values);
29
+ }
30
+
31
+ function normalizeHostMetadata(harmony, record) {
32
+ return Object.fromEntries(HostMetadataFields.map(field => [
33
+ field,
34
+ normalizeIdentifiers(harmony[field], field, record.packageName),
35
+ ]));
36
+ }
37
+
38
+ export {
39
+ HostMetadataFields,
40
+ normalizeHostMetadata,
41
+ };
@@ -0,0 +1,184 @@
1
+ import * as path from 'node:path';
2
+
3
+ import { HarmonyAutolinkingError } from '../errors';
4
+ import type { HarmonyModuleMetadata, HostMetadata, RnohMetadata } from '../types';
5
+ import { normalizeHostMetadata } from './host';
6
+ import { isObject, requireNonEmptyString } from '../utilities/values';
7
+
8
+ const ArkTsIdentifierPattern = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
9
+ const CppIdentifierPattern = /^[A-Za-z_][A-Za-z0-9_]*$/;
10
+ const CmakeTargetPattern = /^[A-Za-z0-9_.:+-]+$/;
11
+ const OhPackagePartPattern = /^[A-Za-z0-9._~-]+$/;
12
+
13
+ interface MetadataRecord {
14
+ readonly packageName: string;
15
+ readonly packageVersion?: string;
16
+ }
17
+
18
+ type UnknownRecord = Record<string, unknown>;
19
+
20
+ function metadataError(message: string, record: MetadataRecord): never {
21
+ throw new HarmonyAutolinkingError('INVALID_METADATA', message, {
22
+ packageName: record.packageName,
23
+ stage: 'metadata-normalize',
24
+ });
25
+ }
26
+
27
+ function requireObject(value: unknown, message: string, record: MetadataRecord): UnknownRecord {
28
+ if (!isObject(value)) metadataError(message, record);
29
+ return value as UnknownRecord;
30
+ }
31
+
32
+ function isValidOhpmPackageName(value: unknown): value is string {
33
+ if (typeof value !== 'string' || !value || /[\0\r\n\\'"`;${}()<>\s]/u.test(value)) return false;
34
+ const scoped = value.startsWith('@');
35
+ const parts = scoped ? value.slice(1).split('/') : value.split('/');
36
+ return parts.length === (scoped ? 2 : 1)
37
+ && parts.every(part => part !== '.' && part !== '..' && OhPackagePartPattern.test(part));
38
+ }
39
+
40
+ function normalizeOhPackageName(
41
+ value: unknown,
42
+ record: MetadataRecord
43
+ ): RnohMetadata['ohPackageName'] {
44
+ if (value === undefined) return undefined;
45
+ if (typeof value === 'string') {
46
+ if (!isValidOhpmPackageName(value)) {
47
+ metadataError('package.json#harmony.autolinking.ohPackageName must be a safe OHPM package name.', record);
48
+ }
49
+ return value;
50
+ }
51
+ if (!Array.isArray(value)) {
52
+ metadataError('package.json#harmony.autolinking.ohPackageName must be a string or HAR mapping array.', record);
53
+ }
54
+
55
+ const seen = new Set<string>();
56
+ return value.map((rawMapping, index) => {
57
+ const field = `package.json#harmony.autolinking.ohPackageName[${index}]`;
58
+ const mapping = requireObject(rawMapping, `${field} must be an object.`, record);
59
+ const harName = requireNonEmptyString(mapping.harName, `${field}.harName`, {
60
+ packageName: record.packageName,
61
+ stage: 'metadata-normalize',
62
+ });
63
+ if (path.posix.basename(harName) !== harName
64
+ || path.win32.basename(harName) !== harName
65
+ || path.posix.extname(harName).toLowerCase() !== '.har') {
66
+ metadataError(`${field}.harName must be a HAR filename.`, record);
67
+ }
68
+ if (seen.has(harName)) metadataError(`${field}.harName is declared more than once.`, record);
69
+ seen.add(harName);
70
+ if (!isValidOhpmPackageName(mapping.packageName)) {
71
+ metadataError(`${field}.packageName must be a safe OHPM package name.`, record);
72
+ }
73
+ const version = mapping.version === undefined
74
+ ? undefined
75
+ : requireNonEmptyString(mapping.version, `${field}.version`, {
76
+ packageName: record.packageName,
77
+ stage: 'metadata-normalize',
78
+ });
79
+ return { harName, packageName: mapping.packageName, ...(version ? { version } : {}) };
80
+ });
81
+ }
82
+
83
+ function normalizeArkTsModules(value: unknown, record: MetadataRecord): ReadonlyArray<string> {
84
+ if (value === undefined) return [];
85
+ if (!Array.isArray(value)) {
86
+ metadataError('expo-module.config.json#harmony.modules must be an array.', record);
87
+ }
88
+
89
+ const seen = new Set<string>();
90
+ const modules = value.map((item, index) => {
91
+ const moduleClass = requireNonEmptyString(
92
+ item,
93
+ `expo-module.config.json#harmony.modules[${index}]`,
94
+ { packageName: record.packageName, stage: 'metadata-normalize' }
95
+ );
96
+ if (!ArkTsIdentifierPattern.test(moduleClass)) {
97
+ metadataError(`expo-module.config.json#harmony.modules[${index}] must be an ArkTS identifier.`, record);
98
+ }
99
+ if (seen.has(moduleClass)) {
100
+ metadataError(`expo-module.config.json#harmony.modules declares ${moduleClass} more than once.`, record);
101
+ }
102
+ seen.add(moduleClass);
103
+ return moduleClass;
104
+ });
105
+ return modules.sort((left, right) => left.localeCompare(right, 'en'));
106
+ }
107
+
108
+ function normalizeHarmonyModuleMetadata(
109
+ harmonyValue: unknown,
110
+ record: MetadataRecord
111
+ ): HarmonyModuleMetadata {
112
+ const harmony = harmonyValue === undefined
113
+ ? {}
114
+ : requireObject(harmonyValue, 'expo-module.config.json#harmony must be an object.', record);
115
+ const host = normalizeHostMetadata(harmony, record) as unknown as HostMetadata;
116
+ return {
117
+ ...host,
118
+ modules: normalizeArkTsModules(harmony.modules, record),
119
+ };
120
+ }
121
+
122
+ function optionalIdentifier(
123
+ value: unknown,
124
+ field: string,
125
+ pattern: RegExp,
126
+ record: MetadataRecord
127
+ ): string | undefined {
128
+ if (value === undefined) return undefined;
129
+ const normalized = requireNonEmptyString(value, field, {
130
+ packageName: record.packageName,
131
+ stage: 'metadata-normalize',
132
+ });
133
+ if (!pattern.test(normalized)) metadataError(`${field} is invalid.`, record);
134
+ return normalized;
135
+ }
136
+
137
+ /** Normalizes current RNOH package metadata, independently of Expo module config. */
138
+ function normalizeRnohPackageMetadata(
139
+ value: unknown,
140
+ record: MetadataRecord
141
+ ): Omit<RnohMetadata, 'harPaths'> {
142
+ if (value == null || value === true) return {};
143
+ const rnoh = requireObject(value, 'package.json#harmony.autolinking must be an object.', record);
144
+ const etsPackageImport = rnoh.etsPackageImport;
145
+ if (etsPackageImport !== undefined && etsPackageImport !== 'default' && etsPackageImport !== 'named') {
146
+ metadataError('package.json#harmony.autolinking.etsPackageImport must be "default" or "named".', record);
147
+ }
148
+ return {
149
+ ohPackageName: normalizeOhPackageName(rnoh.ohPackageName, record),
150
+ ...(rnoh.mainHarPath === undefined
151
+ ? {}
152
+ : {
153
+ mainHarPath: requireNonEmptyString(rnoh.mainHarPath, 'package.json#harmony.autolinking.mainHarPath', {
154
+ packageName: record.packageName,
155
+ stage: 'metadata-normalize',
156
+ }),
157
+ }),
158
+ etsPackageClassName: optionalIdentifier(
159
+ rnoh.etsPackageClassName,
160
+ 'package.json#harmony.autolinking.etsPackageClassName',
161
+ ArkTsIdentifierPattern,
162
+ record
163
+ ),
164
+ etsPackageImport: etsPackageImport as 'default' | 'named' | undefined,
165
+ cppPackageClassName: optionalIdentifier(
166
+ rnoh.cppPackageClassName,
167
+ 'package.json#harmony.autolinking.cppPackageClassName',
168
+ CppIdentifierPattern,
169
+ record
170
+ ),
171
+ cmakeLibraryTargetName: optionalIdentifier(
172
+ rnoh.cmakeLibraryTargetName,
173
+ 'package.json#harmony.autolinking.cmakeLibraryTargetName',
174
+ CmakeTargetPattern,
175
+ record
176
+ ),
177
+ };
178
+ }
179
+
180
+ export {
181
+ isValidOhpmPackageName,
182
+ normalizeHarmonyModuleMetadata,
183
+ normalizeRnohPackageMetadata,
184
+ };
package/src/types.ts ADDED
@@ -0,0 +1,220 @@
1
+ export type Platform = 'harmony';
2
+ export type BuildType = 'debug' | 'release';
3
+ export type ModuleSource
4
+ = 'dependency'
5
+ | 'searchPath'
6
+ | 'nativeModulesDir'
7
+ | 'reactNativeProjectConfig';
8
+
9
+ export interface ArkTsModulePackage {
10
+ /** Conventional package-relative HAR exporting the ArkTS module classes. */
11
+ readonly harPath: string;
12
+ /** OHPM package name read from the Harmony library manifest. */
13
+ readonly ohPackageName: string;
14
+ }
15
+
16
+ export interface HostMetadata {
17
+ readonly rootViewComponents: ReadonlyArray<string>;
18
+ }
19
+
20
+ export type ExpoMetadata = HostMetadata;
21
+
22
+ export interface RnohMetadata {
23
+ readonly ohPackageName?: string | ReadonlyArray<{
24
+ readonly harName: string;
25
+ readonly packageName: string;
26
+ readonly version?: string;
27
+ }>;
28
+ readonly mainHarPath?: string;
29
+ readonly harPaths: ReadonlyArray<string>;
30
+ readonly etsPackageClassName?: string;
31
+ readonly etsPackageImport?: 'default' | 'named';
32
+ readonly cppPackageClassName?: string;
33
+ readonly cmakeLibraryTargetName?: string;
34
+ }
35
+
36
+ export interface HarmonyModuleMetadata extends HostMetadata {
37
+ /** ArkTS Module classes exported by the conventional Harmony library. */
38
+ readonly modules: ReadonlyArray<string>;
39
+ }
40
+
41
+ export interface FixedHvigorBuildDescriptor {
42
+ readonly executable: 'hvigorw';
43
+ readonly cwd: string;
44
+ readonly args: ReadonlyArray<string>;
45
+ }
46
+
47
+ export type ModuleArtifactDescriptor = {
48
+ readonly kind: 'bundled';
49
+ readonly harPaths: ReadonlyArray<string>;
50
+ } | {
51
+ readonly kind: 'local-source';
52
+ readonly outputPath: string;
53
+ readonly materialized: false;
54
+ readonly build: FixedHvigorBuildDescriptor;
55
+ };
56
+
57
+ export interface ModuleDescriptor {
58
+ readonly packageName: string;
59
+ readonly packageVersion: string;
60
+ readonly packageRoot: string;
61
+ /** Reachable package path used to install the bundled HAR during a native build. */
62
+ readonly packageLinkPath: string;
63
+ readonly source: ModuleSource;
64
+ /** Host extension declarations consumed by the generated application provider. */
65
+ readonly expo: ExpoMetadata;
66
+ /** RNOH package metadata read from package.json#harmony.autolinking. */
67
+ readonly rnoh: RnohMetadata;
68
+ /** Expo module author metadata from expo-module.config.json#harmony. */
69
+ readonly harmony: HarmonyModuleMetadata;
70
+ /** Build/package data derived from the conventional Harmony project, never author config. */
71
+ readonly arkTs?: ArkTsModulePackage;
72
+ readonly artifact: ModuleArtifactDescriptor;
73
+ }
74
+
75
+ export interface SearchRecord {
76
+ readonly packageName: string;
77
+ readonly packageVersion: string;
78
+ readonly packageRoot: string;
79
+ readonly source: ModuleSource;
80
+ readonly supportsHarmony: boolean;
81
+ readonly expoModuleConfig: Readonly<Record<string, unknown>> | null;
82
+ readonly rnohMetadata: unknown;
83
+ readonly revision: {
84
+ readonly depth: number | null;
85
+ readonly originPath: string;
86
+ };
87
+ }
88
+
89
+ export interface SearchResult {
90
+ readonly platform: Platform;
91
+ readonly modules: ReadonlyArray<SearchRecord>;
92
+ readonly duplicates: ReadonlyArray<{
93
+ readonly packageName: string;
94
+ readonly revisions: ReadonlyArray<{ readonly version: string; readonly path: string }>;
95
+ }>;
96
+ readonly missingIncludes: ReadonlyArray<string>;
97
+ readonly options: {
98
+ readonly projectRoot: string;
99
+ readonly searchPaths: ReadonlyArray<string>;
100
+ readonly nativeModulesDir: string | null;
101
+ readonly exclude: ReadonlyArray<string>;
102
+ readonly include: ReadonlyArray<string>;
103
+ readonly buildType: BuildType;
104
+ };
105
+ }
106
+
107
+ export interface DiagnosticSource {
108
+ readonly packageName: string;
109
+ readonly packageVersion: string;
110
+ readonly packageRoot: string;
111
+ readonly field?: string;
112
+ }
113
+
114
+ export interface Diagnostic {
115
+ readonly severity: 'error' | 'warning';
116
+ readonly code: string;
117
+ readonly message: string;
118
+ readonly packageName?: string;
119
+ readonly stage: string;
120
+ readonly sources?: ReadonlyArray<DiagnosticSource>;
121
+ }
122
+
123
+ export interface VerificationResult {
124
+ readonly valid: boolean;
125
+ readonly diagnostics: ReadonlyArray<Diagnostic>;
126
+ readonly modules: ReadonlyArray<ModuleDescriptor>;
127
+ }
128
+
129
+ export interface Manifest {
130
+ readonly schemaVersion: 3;
131
+ readonly platform: 'harmony';
132
+ readonly buildType: BuildType;
133
+ readonly modules: ReadonlyArray<ModuleDescriptor>;
134
+ readonly managedArtifacts: ReadonlyArray<string>;
135
+ }
136
+
137
+ export interface CanonicalizeAutolinkingArtifactsOptions {
138
+ readonly manifestSource: string;
139
+ readonly ohPackageSource: string;
140
+ readonly generatedProjectRoot: string;
141
+ readonly canonicalProjectRoot: string;
142
+ readonly harmonyProjectPath?: string;
143
+ }
144
+
145
+ export interface CanonicalizedAutolinkingArtifacts {
146
+ readonly manifest: Manifest;
147
+ readonly manifestSource: string;
148
+ readonly ohPackageSource: string;
149
+ }
150
+
151
+ export interface Logger {
152
+ debug?(message: string, details?: unknown): void;
153
+ info?(message: string, details?: unknown): void;
154
+ warn?(message: string, details?: unknown): void;
155
+ error?(message: string, details?: unknown): void;
156
+ }
157
+
158
+ export interface SearchOptions {
159
+ projectRoot?: string;
160
+ platform?: Platform;
161
+ searchPaths?: ReadonlyArray<string>;
162
+ nativeModulesDir?: string | null;
163
+ exclude?: ReadonlyArray<string>;
164
+ include?: ReadonlyArray<string>;
165
+ buildType?: BuildType;
166
+ logger?: Logger;
167
+ }
168
+
169
+ export interface ResolveOptions extends SearchOptions {
170
+ searchResult?: SearchResult;
171
+ }
172
+
173
+ export interface VerifyOptions extends ResolveOptions {
174
+ modules?: ReadonlyArray<ModuleDescriptor>;
175
+ }
176
+
177
+ export interface LinkOptions extends VerifyOptions {
178
+ projectRoot: string;
179
+ harmonyProjectPath: string;
180
+ nodeModulesPath?: string;
181
+ reactNativeExecutable?: string;
182
+ rnohCliPackageJsonPath?: string;
183
+ timeoutMs?: number;
184
+ outputLimit?: number;
185
+ env?: NodeJS.ProcessEnv;
186
+ }
187
+
188
+ export interface MaterializeLocalSourceOptions {
189
+ readonly projectRoot: string;
190
+ readonly module: ModuleDescriptor;
191
+ readonly timeoutMs?: number;
192
+ readonly outputLimit?: number;
193
+ readonly env?: NodeJS.ProcessEnv;
194
+ }
195
+
196
+ export interface MaterializedLocalSource {
197
+ readonly packageName: string;
198
+ readonly harPath: string;
199
+ }
200
+
201
+ export interface LinkResult {
202
+ readonly platform: Platform;
203
+ readonly modules: ReadonlyArray<ModuleDescriptor>;
204
+ readonly buildType: BuildType;
205
+ readonly managedArtifacts: ReadonlyArray<string>;
206
+ readonly changedArtifacts: ReadonlyArray<string>;
207
+ readonly unchangedArtifacts: ReadonlyArray<string>;
208
+ readonly diagnostics: ReadonlyArray<Diagnostic>;
209
+ readonly warnings: ReadonlyArray<Diagnostic>;
210
+ }
211
+
212
+ export interface AutolinkingErrorJson {
213
+ readonly name: 'HarmonyAutolinkingError';
214
+ readonly code: string;
215
+ readonly message: string;
216
+ readonly stage: string;
217
+ readonly packageName?: string;
218
+ readonly diagnostics?: ReadonlyArray<Diagnostic>;
219
+ readonly details?: unknown;
220
+ }
@@ -0,0 +1,190 @@
1
+ import { createRequire } from 'node:module';
2
+ import * as fs from 'node:fs';
3
+ import * as path from 'node:path';
4
+
5
+ import { HarmonyAutolinkingError } from '../errors';
6
+
7
+ function isObject(value) {
8
+ return value != null && typeof value === 'object' && !Array.isArray(value);
9
+ }
10
+
11
+ function compareText(left, right) {
12
+ return left < right ? -1 : left > right ? 1 : 0;
13
+ }
14
+
15
+ function sortedUniqueStrings(items: ReadonlyArray<string>): string[] {
16
+ return [...new Set(items)].sort(compareText);
17
+ }
18
+
19
+ function uniqueStrings(items: ReadonlyArray<string>): string[] {
20
+ return [...new Set(items)];
21
+ }
22
+
23
+ function stringifyJson(value) {
24
+ return `${JSON.stringify(value, null, 2)}\n`;
25
+ }
26
+
27
+ function normalizeSlashes(value) {
28
+ return value.split(path.sep).join('/');
29
+ }
30
+
31
+ async function readJsonAsync(file, code = 'INVALID_METADATA', stage = 'metadata') {
32
+ try {
33
+ return JSON.parse(await fs.promises.readFile(file, 'utf8'));
34
+ } catch (cause) {
35
+ throw new HarmonyAutolinkingError(code, `Unable to read valid JSON from ${file}.`, { cause, stage });
36
+ }
37
+ }
38
+
39
+ async function pathExistsAsync(target) {
40
+ try {
41
+ await fs.promises.access(target);
42
+ return true;
43
+ } catch (_cause) {
44
+ return false;
45
+ }
46
+ }
47
+
48
+ function resolvePackageFromProject(projectRoot, specifier) {
49
+ return createRequire(path.join(projectRoot, 'package.json')).resolve(specifier);
50
+ }
51
+
52
+ function isPathInside(parent, child) {
53
+ const relative = path.relative(parent, child);
54
+ return relative === '' || (!relative.startsWith(`..${path.sep}`) && relative !== '..' && !path.isAbsolute(relative));
55
+ }
56
+
57
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
58
+ async function realpathExistingAsync(target, options: Record<string, any> = {}) {
59
+ const type = options.type || 'path';
60
+ const code = options.code || 'INVALID_OPTIONS';
61
+ const stage = options.stage || 'validation';
62
+ const field = options.field || 'path';
63
+
64
+ try {
65
+ const resolved = await fs.promises.realpath(path.resolve(target));
66
+ const stat = await fs.promises.stat(resolved);
67
+
68
+ if (type === 'directory' && !stat.isDirectory()) throw new TypeError('not a directory');
69
+ if (type === 'file' && !stat.isFile()) throw new TypeError('not a file');
70
+ return resolved;
71
+ } catch (cause) {
72
+ throw new HarmonyAutolinkingError(code, `${field} does not reference an existing ${type}: ${target}`, {
73
+ cause,
74
+ stage,
75
+ });
76
+ }
77
+ }
78
+
79
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
80
+ async function resolveInsideAsync(root, relative, field, options: Record<string, any> = {}) {
81
+ const mustExist = options.mustExist !== false;
82
+ const type = options.type || 'path';
83
+ const packageName = options.packageName;
84
+
85
+ if (typeof relative !== 'string' || !relative.trim() || path.isAbsolute(relative)) {
86
+ throw new HarmonyAutolinkingError('INVALID_METADATA', `${field} must be a non-empty package-relative path.`, {
87
+ packageName,
88
+ stage: 'metadata',
89
+ });
90
+ }
91
+ const realRoot = await fs.promises.realpath(root);
92
+ const lexical = path.resolve(realRoot, relative);
93
+
94
+ if (!isPathInside(realRoot, lexical)) {
95
+ throw new HarmonyAutolinkingError('PATH_OUTSIDE_PACKAGE', `${field} escapes package root.`, {
96
+ packageName,
97
+ stage: 'metadata',
98
+ });
99
+ }
100
+ if (!mustExist) return lexical;
101
+
102
+ let resolved;
103
+ try {
104
+ resolved = await fs.promises.realpath(lexical);
105
+ } catch (cause) {
106
+ throw new HarmonyAutolinkingError('INVALID_METADATA', `${field} does not exist: ${relative}`, {
107
+ cause,
108
+ packageName,
109
+ stage: 'metadata',
110
+ });
111
+ }
112
+
113
+ if (!isPathInside(realRoot, resolved)) {
114
+ throw new HarmonyAutolinkingError('PATH_OUTSIDE_PACKAGE', `${field} resolves outside package root.`, {
115
+ packageName,
116
+ stage: 'metadata',
117
+ });
118
+ }
119
+ const stat = await fs.promises.stat(resolved);
120
+
121
+ if (type === 'file' && !stat.isFile()) {
122
+ throw new HarmonyAutolinkingError('INVALID_METADATA', `${field} must reference a file: ${relative}`, {
123
+ packageName,
124
+ stage: 'metadata',
125
+ });
126
+ }
127
+ if (type === 'directory' && !stat.isDirectory()) {
128
+ throw new HarmonyAutolinkingError('INVALID_METADATA', `${field} must reference a directory: ${relative}`, {
129
+ packageName,
130
+ stage: 'metadata',
131
+ });
132
+ }
133
+ return resolved;
134
+ }
135
+
136
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
137
+ function requireNonEmptyString(value, field, options: Record<string, any> = {}) {
138
+ if (typeof value !== 'string' || !value.trim()) {
139
+ throw new HarmonyAutolinkingError(options.code || 'INVALID_METADATA', `${field} must be a non-empty string.`, {
140
+ packageName: options.packageName,
141
+ stage: options.stage || 'metadata',
142
+ });
143
+ }
144
+ return value;
145
+ }
146
+
147
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
148
+ function normalizeStringArray(value, field, options: Record<string, any> = {}) {
149
+ if (value == null && options.optional) return [];
150
+
151
+ const values = Array.isArray(value) ? value : options.allowScalar ? [value] : null;
152
+ if (!values || values.some(item => typeof item !== 'string' || !item.trim())) {
153
+ throw new HarmonyAutolinkingError(options.code || 'INVALID_OPTIONS', `${field} must be an array of non-empty strings.`, {
154
+ packageName: options.packageName,
155
+ stage: options.stage || 'options',
156
+ });
157
+ }
158
+
159
+ return [...new Set(values)];
160
+ }
161
+
162
+ function sanitizeOutput(value, roots = [], limit = 8192) {
163
+ let result = String(value || '');
164
+
165
+ for (const root of roots.filter(Boolean)) result = result.split(root).join('<path>');
166
+ return result.slice(0, limit);
167
+ }
168
+
169
+ function emitLog(logger, level, message, details) {
170
+ if (logger && typeof logger[level] === 'function') logger[level](message, details);
171
+ }
172
+
173
+ export {
174
+ compareText,
175
+ emitLog,
176
+ isObject,
177
+ isPathInside,
178
+ normalizeSlashes,
179
+ pathExistsAsync,
180
+ resolvePackageFromProject,
181
+ readJsonAsync,
182
+ realpathExistingAsync,
183
+ requireNonEmptyString,
184
+ resolveInsideAsync,
185
+ sanitizeOutput,
186
+ stringifyJson,
187
+ sortedUniqueStrings,
188
+ normalizeStringArray,
189
+ uniqueStrings,
190
+ };
package/tsconfig.json ADDED
@@ -0,0 +1,24 @@
1
+ {
2
+ "compilerOptions": {
3
+ "declaration": true,
4
+ "esModuleInterop": true,
5
+ "lib": ["ES2022"],
6
+ "module": "Node16",
7
+ "moduleResolution": "Node16",
8
+ "outDir": "build",
9
+ "resolveJsonModule": true,
10
+ "skipLibCheck": true,
11
+ "strict": true,
12
+ "noImplicitAny": false,
13
+ "strictNullChecks": false,
14
+ "useUnknownInCatchVariables": false,
15
+ "target": "ES2022",
16
+ "types": ["node"]
17
+ },
18
+ "include": [
19
+ "bin/**/*.ts",
20
+ "index.ts",
21
+ "src/**/*.ts"
22
+ ],
23
+ "exclude": ["build"]
24
+ }