@jbrowse/cli 4.3.0 → 5.0.0-beta.10

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 (71) hide show
  1. package/README.md +451 -72
  2. package/dist/base.js +0 -3
  3. package/dist/bin.js +2 -1
  4. package/dist/cliFetch.js +3 -0
  5. package/dist/commands/add-assembly/index.js +46 -19
  6. package/dist/commands/add-assembly/utils.js +204 -290
  7. package/dist/commands/add-connection.js +73 -79
  8. package/dist/commands/add-track-json.js +24 -12
  9. package/dist/commands/add-track-utils/adapter-utils.js +189 -288
  10. package/dist/commands/add-track-utils/file-operations.js +27 -11
  11. package/dist/commands/add-track-utils/multiwig.js +47 -0
  12. package/dist/commands/add-track-utils/track-config.js +66 -25
  13. package/dist/commands/add-track-utils/validators.js +36 -20
  14. package/dist/commands/add-track.js +200 -62
  15. package/dist/commands/admin-server/index.js +39 -50
  16. package/dist/commands/admin-server/utils.js +238 -126
  17. package/dist/commands/create.js +18 -34
  18. package/dist/commands/make-density/density-generator.js +180 -0
  19. package/dist/commands/make-density/index.js +139 -0
  20. package/dist/commands/make-pif/index.js +133 -11
  21. package/dist/commands/make-pif/pif-generator.js +243 -43
  22. package/dist/commands/process-utils.js +13 -2
  23. package/dist/commands/remove-track.js +20 -13
  24. package/dist/commands/set-default-session.js +35 -31
  25. package/dist/commands/shared/config-operations.js +3 -10
  26. package/dist/commands/shared/density.js +12 -0
  27. package/dist/commands/shared/sort-utils.js +67 -10
  28. package/dist/commands/shared/validators.js +38 -3
  29. package/dist/commands/sort-bed.js +2 -33
  30. package/dist/commands/sort-gff.js +2 -33
  31. package/dist/commands/text-index/adapter-utils.js +19 -26
  32. package/dist/commands/text-index/aggregate.js +40 -34
  33. package/dist/commands/text-index/command.js +37 -7
  34. package/dist/commands/text-index/config-utils.js +62 -85
  35. package/dist/commands/text-index/file-list.js +32 -18
  36. package/dist/commands/text-index/index.js +4 -4
  37. package/dist/commands/text-index/indexing-utils.js +44 -96
  38. package/dist/commands/text-index/per-track.js +44 -31
  39. package/dist/commands/text-index/validators.js +1 -1
  40. package/dist/commands/upgrade.js +20 -32
  41. package/dist/commands/validate/configManifest.generated.js +7613 -0
  42. package/dist/commands/validate/configSchema.generated.js +20400 -0
  43. package/dist/commands/validate/displayDefaultKeys.js +28 -0
  44. package/dist/commands/validate/index.js +113 -0
  45. package/dist/commands/validate/liftConfig.js +34 -0
  46. package/dist/commands/validate/markRules/aggregateFieldName.js +9 -0
  47. package/dist/commands/validate/markRules/autoBin.js +27 -0
  48. package/dist/commands/validate/markRules/colorScale.js +244 -0
  49. package/dist/commands/validate/markRules/cssColorsLevel4.js +162 -0
  50. package/dist/commands/validate/markRules/jexlStrings.js +23 -0
  51. package/dist/commands/validate/markRules/markProblems.js +532 -0
  52. package/dist/commands/validate/markRules/markRuleFacts.js +6 -0
  53. package/dist/commands/validate/markRules/markSpecs.js +96 -0
  54. package/dist/commands/validate/markRules/markVocabulary.js +57 -0
  55. package/dist/commands/validate/markRules/shapeNameList.js +7 -0
  56. package/dist/commands/validate/markRules/stepChannels.js +55 -0
  57. package/dist/commands/validate/schemaValidate.js +425 -0
  58. package/dist/commands/validate/suggest.js +35 -0
  59. package/dist/commands/validate/types.js +6 -0
  60. package/dist/commands/validate/validateConfig.js +502 -0
  61. package/dist/index.js +104 -50
  62. package/dist/types/common.js +3 -27
  63. package/dist/utils.js +309 -128
  64. package/dist/version.js +1 -1
  65. package/package.json +8 -14
  66. package/bin/run +0 -4
  67. package/bundle/index.js +0 -3686
  68. package/dist/commands/make-pif/cigar-utils.js +0 -24
  69. package/dist/commands/make-pif/file-utils.js +0 -30
  70. package/dist/commands/track-utils.js +0 -74
  71. package/dist/util.js +0 -35
@@ -1,302 +1,203 @@
1
- export function makeLocationProtocol(protocol) {
2
- return (location) => {
3
- if (protocol === 'uri') {
1
+ import fs from 'node:fs';
2
+ import { adapterTypesToTrackTypeMap, formats, matchFormat, resolveIndexType, sidecarCandidateNames, trackTypeForAdapter, } from '@jbrowse/add-track-core';
3
+ import { isURL } from '../../types/common.js';
4
+ import { densitySidecarPath } from '../shared/density.js';
5
+ import { configManifest } from '../validate/configManifest.generated.js';
6
+ function hasLocField(spec) {
7
+ return 'locField' in spec;
8
+ }
9
+ const adapterTypeToSpec = {};
10
+ for (const { spec } of formats) {
11
+ if (hasLocField(spec) && !adapterTypeToSpec[spec.adapterType]) {
12
+ adapterTypeToSpec[spec.adapterType] = spec;
13
+ }
14
+ }
15
+ /**
16
+ * The sidecar actually sitting beside `location`, or the conventional name when
17
+ * none of the spellings is there — so a missing index still reports the file
18
+ * that was expected, and `--load` still names it.
19
+ *
20
+ * Probed with `existsSync`, which is false for every candidate of a URL and so
21
+ * leaves a remote track with exactly the conventional guess it had before this
22
+ * existed. A remote `.csi` still wants `--indexFile`.
23
+ */
24
+ export function siblingSidecar(location, suffix) {
25
+ const candidates = sidecarCandidateNames(location, suffix);
26
+ return candidates.find(c => fs.existsSync(c)) ?? candidates[0];
27
+ }
28
+ // The adapters that declare a `densityAdapter` slot, read off the generated
29
+ // manifest `jbrowse validate` uses. The hand-written list missed `GWASAdapter`,
30
+ // which inherits the slot through `bedTabixConfigSchema` instead of spreading
31
+ // `densityAdapterConfigSchemaFields`, so `--density` on a Pan-UKBB file was
32
+ // refused by a message that listed nine adapters and not the tenth.
33
+ const densityAdapterTypes = new Set(Object.entries(configManifest.adapters)
34
+ .filter(([, entry]) => entry.slots.some(s => s.name === 'densityAdapter'))
35
+ .map(([name]) => name)
36
+ .sort());
37
+ /**
38
+ * The adapter with its density sidecar attached, and the local file to load
39
+ * beside the config when there is one.
40
+ *
41
+ * An explicit `--density` is taken as given, since a URL cannot be probed;
42
+ * otherwise the conventional `<file>.density.bw` counts only when it is
43
+ * actually there, the way `siblingSidecar` probes an index.
44
+ */
45
+ export function withDensityAdapter({ adapter, location, density, makeLocation, }) {
46
+ const supported = densityAdapterTypes.has(adapter.type);
47
+ if (density !== undefined && !supported) {
48
+ throw new Error(`--density has no slot on ${adapter.type}. The adapters carrying a density sidecar are: ${[...densityAdapterTypes].join(', ')}`);
49
+ }
50
+ const probed = location !== undefined && fs.existsSync(densitySidecarPath(location))
51
+ ? densitySidecarPath(location)
52
+ : undefined;
53
+ if (probed !== undefined && !supported) {
54
+ console.warn(`Warning: ${probed} sits beside the track file but ${adapter.type} has no densityAdapter slot, so it is not attached. The adapters carrying one are indexed: bgzip and tabix the file, then add it.`);
55
+ }
56
+ const file = density === undefined ? (supported ? probed : undefined) : density;
57
+ return file === undefined
58
+ ? { adapter }
59
+ : {
60
+ adapter: {
61
+ ...adapter,
62
+ densityAdapter: {
63
+ type: 'BigWigAdapter',
64
+ bigWigLocation: makeLocation(file),
65
+ },
66
+ },
67
+ file: isURL(file) ? undefined : file,
68
+ };
69
+ }
70
+ // builds the adapter object and the list of source files for a spec in one
71
+ // place, so the config it writes and the files add-track copies can never drift
72
+ function buildFromSpec(spec, { location, index, bed1, bed2, makeLocation }) {
73
+ switch (spec.kind) {
74
+ case 'single':
75
+ return {
76
+ adapter: {
77
+ type: spec.adapterType,
78
+ [spec.locField]: makeLocation(location),
79
+ },
80
+ files: [location],
81
+ };
82
+ case 'indexed': {
83
+ const idx = index || siblingSidecar(location, spec.suffix);
4
84
  return {
5
- uri: location,
6
- locationType: 'UriLocation',
85
+ adapter: {
86
+ type: spec.adapterType,
87
+ [spec.locField]: makeLocation(location),
88
+ index: {
89
+ location: makeLocation(idx),
90
+ // the type follows the file that was CHOSEN, not the one the user
91
+ // typed, so a detected `.csi` is opened as one
92
+ indexType: resolveIndexType(idx, spec.indexType),
93
+ },
94
+ },
95
+ files: [location, idx],
7
96
  };
8
97
  }
9
- if (protocol === 'localPath') {
98
+ case 'sidecar': {
99
+ const sidecars = spec.sidecars.map(s => ({
100
+ field: s.field,
101
+ path: s.fromIndex && index ? index : siblingSidecar(location, s.suffix),
102
+ }));
10
103
  return {
11
- localPath: location,
12
- locationType: 'LocalPathLocation',
104
+ adapter: {
105
+ type: spec.adapterType,
106
+ [spec.locField]: makeLocation(location),
107
+ ...Object.fromEntries(sidecars.map(s => [s.field, makeLocation(s.path)])),
108
+ },
109
+ files: [location, ...sidecars.map(s => s.path)],
13
110
  };
14
111
  }
112
+ case 'anchors':
113
+ return {
114
+ adapter: {
115
+ type: spec.adapterType,
116
+ [spec.locField]: makeLocation(location),
117
+ bed1Location: bed1 ? makeLocation(bed1) : undefined,
118
+ bed2Location: bed2 ? makeLocation(bed2) : undefined,
119
+ },
120
+ files: [location, bed1, bed2],
121
+ };
122
+ case 'unsupported':
123
+ return { adapter: { type: 'UNSUPPORTED' }, files: [] };
124
+ }
125
+ }
126
+ export function makeLocationProtocol(protocol) {
127
+ return (location) => {
128
+ if (protocol === 'uri') {
129
+ return { uri: location, locationType: 'UriLocation' };
130
+ }
131
+ if (protocol === 'localPath') {
132
+ return { localPath: location, locationType: 'LocalPathLocation' };
133
+ }
15
134
  throw new Error(`invalid protocol ${protocol}`);
16
135
  };
17
136
  }
18
- export function guessFileNames({ location, index, bed1, bed2, }) {
19
- if (/\.anchors(\.simple)?$/i.test(location)) {
20
- return {
21
- file: location,
22
- bed1: bed1,
23
- bed2: bed2,
24
- };
25
- }
26
- else if (/\.bam$/i.test(location)) {
27
- return {
28
- file: location,
29
- index: index || `${location}.bai`,
30
- };
31
- }
32
- else if (/\.cram$/i.test(location)) {
33
- return {
34
- file: location,
35
- index: index || `${location}.crai`,
36
- };
37
- }
38
- else if (/\.gff3?\.b?gz$/i.test(location) ||
39
- /\.vcf\.b?gz$/i.test(location) ||
40
- /\.bed\.b?gz$/i.test(location) ||
41
- /\.bedmethyl\.gz$/i.test(location) ||
42
- /\.pif\.b?gz$/i.test(location)) {
43
- return {
44
- file: location,
45
- index: index || `${location}.tbi`,
46
- };
47
- }
48
- else if (/\.(fa|fasta|fas|fna|mfa)$/i.test(location)) {
49
- return {
50
- file: location,
51
- index: index || `${location}.fai`,
52
- };
53
- }
54
- else if (/\.(fa|fasta|fas|fna|mfa)\.b?gz$/i.test(location)) {
55
- return {
56
- file: location,
57
- index: `${location}.fai`,
58
- index2: `${location}.gzi`,
59
- };
60
- }
61
- else if (/\.2bit$/i.test(location) ||
62
- /\.bedpe(\.gz)?$/i.test(location) ||
63
- /\/trackData.jsonz?$/i.test(location) ||
64
- /\/sparql$/i.test(location) ||
65
- /\.out(\.gz)?$/i.test(location) ||
66
- /\.paf(\.gz)?$/i.test(location) ||
67
- /\.delta(\.gz)?$/i.test(location) ||
68
- /\.bed$/i.test(location) ||
69
- /\.(bw|bigwig)$/i.test(location) ||
70
- /\.(bb|bigbed)$/i.test(location) ||
71
- /\.vcf$/i.test(location) ||
72
- /\.gtf$/i.test(location) ||
73
- /\.gff3?$/i.test(location) ||
74
- /\.chain(\.gz)?$/i.test(location) ||
75
- /\.hic$/i.test(location)) {
76
- return {
77
- file: location,
78
- };
79
- }
80
- return {};
137
+ /**
138
+ * The bare filename the format table matches against — the same thing
139
+ * `@jbrowse/core`'s `getFileName` hands the guesser chain, so a path and a URL
140
+ * with a presigned query string both reduce to what the regexes expect.
141
+ */
142
+ export function fileNameOf(location) {
143
+ return (location.replaceAll('\\', '/').split('/').at(-1)?.split(/[?#]/)[0] ?? '');
81
144
  }
82
- export function guessAdapter({ location, protocol, index, bed1, bed2, }) {
83
- const makeLocation = makeLocationProtocol(protocol);
84
- if (/\.bam$/i.test(location)) {
85
- return {
86
- type: 'BamAdapter',
87
- bamLocation: makeLocation(location),
88
- index: {
89
- location: makeLocation(index || `${location}.bai`),
90
- indexType: index?.toUpperCase().endsWith('CSI') ? 'CSI' : 'BAI',
91
- },
92
- };
93
- }
94
- else if (/\.cram$/i.test(location)) {
95
- return {
96
- type: 'CramAdapter',
97
- cramLocation: makeLocation(location),
98
- craiLocation: makeLocation(`${location}.crai`),
99
- };
100
- }
101
- else if (/\.gff3?$/i.test(location)) {
102
- return {
103
- type: 'Gff3Adapter',
104
- gffLocation: makeLocation(location),
105
- };
106
- }
107
- else if (/\.gff3?\.b?gz$/i.test(location)) {
108
- return {
109
- type: 'Gff3TabixAdapter',
110
- gffGzLocation: makeLocation(location),
111
- index: {
112
- location: makeLocation(index || `${location}.tbi`),
113
- indexType: index?.toUpperCase().endsWith('CSI') ? 'CSI' : 'TBI',
114
- },
115
- };
116
- }
117
- else if (/\.gtf$/i.test(location)) {
118
- return {
119
- type: 'GtfAdapter',
120
- gtfLocation: makeLocation(location),
121
- };
122
- }
123
- else if (/\.vcf$/i.test(location)) {
124
- return {
125
- type: 'VcfAdapter',
126
- vcfLocation: makeLocation(location),
127
- };
128
- }
129
- else if (/\.vcf\.b?gz$/i.test(location)) {
130
- return {
131
- type: 'VcfTabixAdapter',
132
- vcfGzLocation: makeLocation(location),
133
- index: {
134
- location: makeLocation(index || `${location}.tbi`),
135
- indexType: index?.toUpperCase().endsWith('CSI') ? 'CSI' : 'TBI',
136
- },
137
- };
138
- }
139
- else if (/\.vcf\.idx$/i.test(location)) {
140
- return {
141
- type: 'UNSUPPORTED',
142
- };
143
- }
144
- else if (/\.bedpe(.gz)?$/i.test(location)) {
145
- return {
146
- type: 'BedpeAdapter',
147
- bedpeLocation: makeLocation(location),
148
- };
149
- }
150
- else if (/\.bed$/i.test(location)) {
151
- return {
152
- type: 'BedAdapter',
153
- bedLocation: makeLocation(location),
154
- };
155
- }
156
- else if (/\.pif\.b?gz$/i.test(location)) {
157
- return {
158
- type: 'PairwiseIndexedPAFAdapter',
159
- pifGzLocation: makeLocation(location),
160
- index: {
161
- location: makeLocation(index || `${location}.tbi`),
162
- indexType: index?.toUpperCase().endsWith('CSI') ? 'CSI' : 'TBI',
163
- },
164
- };
165
- }
166
- else if (/\.bedmethyl\.gz$/i.test(location) ||
167
- /\.bed\.b?gz$/i.test(location)) {
168
- return {
169
- type: 'BedTabixAdapter',
170
- bedGzLocation: makeLocation(location),
171
- index: {
172
- location: makeLocation(index || `${location}.tbi`),
173
- indexType: index?.toUpperCase().endsWith('CSI') ? 'CSI' : 'TBI',
174
- },
175
- };
176
- }
177
- else if (/\.(bb|bigbed)$/i.test(location)) {
178
- return {
179
- type: 'BigBedAdapter',
180
- bigBedLocation: makeLocation(location),
181
- };
182
- }
183
- else if (/\.(bw|bigwig)$/i.test(location)) {
184
- return {
185
- type: 'BigWigAdapter',
186
- bigWigLocation: makeLocation(location),
187
- };
188
- }
189
- else if (/\.(fa|fasta|fna|mfa)$/i.test(location)) {
190
- return {
191
- type: 'IndexedFastaAdapter',
192
- fastaLocation: makeLocation(location),
193
- faiLocation: makeLocation(index || `${location}.fai`),
194
- };
195
- }
196
- else if (/\.(fa|fasta|fna|mfa)\.b?gz$/i.test(location)) {
197
- return {
198
- type: 'BgzipFastaAdapter',
199
- fastaLocation: makeLocation(location),
200
- faiLocation: makeLocation(`${location}.fai`),
201
- gziLocation: makeLocation(`${location}.gzi`),
202
- };
203
- }
204
- else if (/\.2bit$/i.test(location)) {
205
- return {
206
- type: 'TwoBitAdapter',
207
- twoBitLocation: makeLocation(location),
208
- };
209
- }
210
- else if (/\.sizes$/i.test(location)) {
211
- return {
212
- type: 'UNSUPPORTED',
213
- };
214
- }
215
- else if (/\/trackData.jsonz?$/i.test(location)) {
216
- return {
217
- type: 'NCListAdapter',
218
- rootUrlTemplate: makeLocation(location),
219
- };
220
- }
221
- else if (/\/sparql$/i.test(location)) {
222
- return {
223
- type: 'SPARQLAdapter',
224
- endpoint: location,
225
- };
226
- }
227
- else if (/\.hic$/i.test(location)) {
228
- return {
229
- type: 'HicAdapter',
230
- hicLocation: makeLocation(location),
231
- };
232
- }
233
- else if (/\.paf(.gz)?$/i.test(location)) {
234
- return {
235
- type: 'PAFAdapter',
236
- pafLocation: makeLocation(location),
237
- };
238
- }
239
- else if (/\.out(.gz)?$/i.test(location)) {
240
- return {
241
- type: 'MashMapAdapter',
242
- outLocation: makeLocation(location),
243
- };
244
- }
245
- else if (/\.chain(.gz)?$/i.test(location)) {
246
- return {
247
- type: 'ChainAdapter',
248
- chainLocation: makeLocation(location),
249
- };
250
- }
251
- else if (/\.delta(.gz)?$/i.test(location)) {
252
- return {
253
- type: 'DeltaAdapter',
254
- deltaLocation: makeLocation(location),
255
- };
256
- }
257
- else if (/\.anchors(\.gz)?$/i.test(location)) {
258
- return {
259
- type: 'MCScanAnchorsAdapter',
260
- mcscanAnchorsLocation: makeLocation(location),
261
- bed1Location: bed1 ? makeLocation(bed1) : undefined,
262
- bed2Location: bed2 ? makeLocation(bed2) : undefined,
263
- };
145
+ // resolves the file-layout spec that both the adapter config and the copied
146
+ // file set derive from, honoring an explicit --adapterType. `typeOverride` is
147
+ // the label to stamp on the adapter when the extension's layout is reused under
148
+ // a different adapter type.
149
+ function resolveSpec(location, adapterType) {
150
+ const spec = matchFormat(fileNameOf(location))?.spec;
151
+ if (adapterType) {
152
+ const known = adapterTypeToSpec[adapterType];
153
+ if (known) {
154
+ // explicit --adapterType resolves back to its known file layout
155
+ return { spec: known };
156
+ }
157
+ else if (spec) {
158
+ // unknown adapter type on a recognized extension: reuse that extension's
159
+ // file layout (location field + any sidecar/index files) under the new
160
+ // type name, so e.g. --adapterType MyPAFAdapter on a .paf keeps its
161
+ // pafLocation instead of dropping the file. Override with --config if the
162
+ // custom adapter uses a different field.
163
+ return { spec, typeOverride: adapterType };
164
+ }
165
+ else {
166
+ // custom adapter type on an unrecognized extension: no layout to reuse
167
+ return { typeOverride: adapterType };
168
+ }
264
169
  }
265
- else if (/\.anchors\.simple(\.gz)?$/i.test(location)) {
266
- return {
267
- type: 'MCScanSimpleAnchorsAdapter',
268
- mcscanSimpleAnchorsLocation: makeLocation(location),
269
- bed1Location: bed1 ? makeLocation(bed1) : undefined,
270
- bed2Location: bed2 ? makeLocation(bed2) : undefined,
271
- };
170
+ else {
171
+ return { spec };
272
172
  }
273
- return {
274
- type: 'UNKNOWN',
275
- };
276
173
  }
277
- export const adapterTypesToTrackTypeMap = {
278
- BamAdapter: 'AlignmentsTrack',
279
- CramAdapter: 'AlignmentsTrack',
280
- BgzipFastaAdapter: 'ReferenceSequenceTrack',
281
- BigWigAdapter: 'QuantitativeTrack',
282
- BedGraphAdapter: 'QuantitativeTrack',
283
- BedGraphTabixAdapter: 'QuantitativeTrack',
284
- IndexedFastaAdapter: 'ReferenceSequenceTrack',
285
- TwoBitAdapter: 'ReferenceSequenceTrack',
286
- VcfTabixAdapter: 'VariantTrack',
287
- VcfAdapter: 'VariantTrack',
288
- BedpeAdapter: 'VariantTrack',
289
- BedAdapter: 'FeatureTrack',
290
- BedTabixAdapter: 'FeatureTrack',
291
- HicAdapter: 'HicTrack',
292
- PAFAdapter: 'SyntenyTrack',
293
- DeltaAdapter: 'SyntenyTrack',
294
- ChainAdapter: 'SyntenyTrack',
295
- MashMapAdapter: 'SyntenyTrack',
296
- PairwiseIndexedPAFAdapter: 'SyntenyTrack',
297
- MCScanAnchorsAdapter: 'SyntenyTrack',
298
- MCScanSimpleAnchorsAdapter: 'SyntenyTrack',
299
- };
300
- export function guessTrackType(adapterType) {
301
- return adapterTypesToTrackTypeMap[adapterType] || 'FeatureTrack';
174
+ // derives the track adapter and the raw source files together from one spec, so
175
+ // the config written and the files add-track copies can never drift. mapLocation
176
+ // turns a raw source path into the location the adapter stores (relative path +
177
+ // protocol wrapper); the returned files stay raw for the copy step.
178
+ export function guessTrack({ location, index, bed1, bed2, adapterType, mapLocation, }) {
179
+ const { spec, typeOverride } = resolveSpec(location, adapterType);
180
+ if (spec) {
181
+ const { adapter, files } = buildFromSpec(spec, {
182
+ location,
183
+ index,
184
+ bed1,
185
+ bed2,
186
+ makeLocation: mapLocation,
187
+ });
188
+ return {
189
+ adapter: typeOverride ? { ...adapter, type: typeOverride } : adapter,
190
+ files,
191
+ };
192
+ }
193
+ return { adapter: { type: typeOverride ?? 'UNKNOWN' }, files: [] };
194
+ }
195
+ export function guessTrackType(adapterType, location) {
196
+ return (trackTypeForAdapter(adapterType, location && fileNameOf(location)) ||
197
+ 'FeatureTrack');
302
198
  }
199
+ // the synteny adapters are exactly those mapping to a SyntenyTrack, so derive
200
+ // the set instead of maintaining a second hand-written list that can drift
201
+ export const syntenyAdapterTypes = new Set(Object.entries(adapterTypesToTrackTypeMap)
202
+ .filter(([, trackType]) => trackType === 'SyntenyTrack')
203
+ .map(([adapterType]) => adapterType));
@@ -1,21 +1,23 @@
1
- import fs from 'fs';
2
- import path from 'path';
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { ignoreNotFound } from '../../utils.js';
3
4
  const { copyFile, rename, symlink, unlink } = fs.promises;
4
5
  const { COPYFILE_EXCL } = fs.constants;
5
- export async function loadFile({ src, destDir, mode, subDir = '', force = false, }) {
6
+ async function loadFile({ src, destDir, mode, subDir = '', force = false, }) {
6
7
  if (mode === 'inPlace') {
7
8
  return;
8
9
  }
9
10
  const dest = path.join(destDir, subDir, path.basename(src));
11
+ // the file already sits where the config wants it (add-track run from inside
12
+ // the install dir), so there is nothing to copy/move/link. Bailing before the
13
+ // force-unlink matters: dest and src are the same file, so unlinking it
14
+ // deleted the user's data and left the copy with nothing to read. A source
15
+ // that isn't there still has to fail, so fall through to get its ENOENT.
16
+ if (path.resolve(src) === path.resolve(dest) && fs.existsSync(src)) {
17
+ return;
18
+ }
10
19
  if (force) {
11
- try {
12
- await unlink(dest);
13
- }
14
- catch (e) {
15
- if (e && typeof e === 'object' && 'code' in e && e.code !== 'ENOENT') {
16
- throw e;
17
- }
18
- }
20
+ await ignoreNotFound(unlink(dest));
19
21
  }
20
22
  if (mode === 'copy') {
21
23
  return copyFile(src, dest, force ? 0 : COPYFILE_EXCL);
@@ -28,3 +30,17 @@ export async function loadFile({ src, destDir, mode, subDir = '', force = false,
28
30
  }
29
31
  return undefined;
30
32
  }
33
+ // load a set of source files into the config directory, skipping the operation
34
+ // entirely when no load mode is given (e.g. a URL track). undefined entries
35
+ // (optional index/bed files) are ignored
36
+ export async function loadFiles({ files, destDir, mode, subDir, force, }) {
37
+ // inPlace references files where they sit, so it needs no destination dir
38
+ if (mode && mode !== 'inPlace') {
39
+ if (subDir) {
40
+ fs.mkdirSync(path.join(destDir, subDir), { recursive: true });
41
+ }
42
+ await Promise.all(files
43
+ .filter((f) => !!f)
44
+ .map(src => loadFile({ src, destDir, mode, subDir, force })));
45
+ }
46
+ }
@@ -0,0 +1,47 @@
1
+ import fs from 'node:fs';
2
+ import { isURL } from '../../types/common.js';
3
+ // Parse the --multiwig argument into a subadapter list, mirroring @jbrowse/img's
4
+ // --multiwig: either a comma-separated list of BigWig files/URLs, or a `.json`
5
+ // file holding an array of BigWig locations or full subadapter objects (each a
6
+ // BigWigAdapter config carrying its own name/color/group).
7
+ //
8
+ // A string entry becomes an explicit `{ type: 'BigWigAdapter', bigWigLocation }`
9
+ // subadapter — NOT an entry in the adapter's `bigWigs` string-array shorthand.
10
+ // The shorthand can only carry bare URIs and does not resolve a relative path
11
+ // against the config's baseUri, so a relative local file 404s there. Building the
12
+ // location object ourselves runs it through the same mapLocation as every other
13
+ // track file, so `--load copy --subDir bw` yields `{ uri: 'bw/a.bw' }` that
14
+ // resolves relative to config.json exactly like a single-file track.
15
+ //
16
+ // Object entries are full subadapter configs, so they pass through unchanged
17
+ // (keeping their per-row metadata) and own their own locations — use URLs or
18
+ // config-relative uris there, since --load only copies the string entries.
19
+ export function buildMultiWiggle({ sources, mapLocation, }) {
20
+ const files = [];
21
+ const subadapters = parseSources(sources).map(entry => {
22
+ if (typeof entry === 'string') {
23
+ if (!isURL(entry)) {
24
+ files.push(entry);
25
+ }
26
+ return { type: 'BigWigAdapter', bigWigLocation: mapLocation(entry) };
27
+ }
28
+ return entry;
29
+ });
30
+ return {
31
+ adapter: { type: 'MultiWiggleAdapter', subadapters },
32
+ files,
33
+ };
34
+ }
35
+ function parseSources(sources) {
36
+ if (sources.toLowerCase().endsWith('.json')) {
37
+ const data = JSON.parse(fs.readFileSync(sources, 'utf8'));
38
+ if (!Array.isArray(data)) {
39
+ throw new Error(`${sources}: expected a JSON array of BigWig URLs or subadapter objects`);
40
+ }
41
+ return data;
42
+ }
43
+ return sources
44
+ .split(',')
45
+ .map(s => s.trim())
46
+ .filter(Boolean);
47
+ }