@jbrowse/cli 4.3.0 → 5.0.0-beta.2

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 (57) hide show
  1. package/README.md +413 -57
  2. package/bin/run +1 -1
  3. package/bundle/index.js +550 -2659
  4. package/dist/base.js +0 -3
  5. package/dist/bin.js +2 -1
  6. package/dist/cliFetch.js +3 -0
  7. package/dist/commands/add-assembly/index.js +46 -19
  8. package/dist/commands/add-assembly/utils.js +204 -290
  9. package/dist/commands/add-connection.js +73 -79
  10. package/dist/commands/add-track-json.js +24 -12
  11. package/dist/commands/add-track-utils/adapter-utils.js +222 -288
  12. package/dist/commands/add-track-utils/file-operations.js +27 -11
  13. package/dist/commands/add-track-utils/multiwig.js +47 -0
  14. package/dist/commands/add-track-utils/track-config.js +68 -25
  15. package/dist/commands/add-track-utils/validators.js +36 -20
  16. package/dist/commands/add-track.js +201 -62
  17. package/dist/commands/admin-server/index.js +30 -49
  18. package/dist/commands/admin-server/utils.js +235 -125
  19. package/dist/commands/create.js +18 -34
  20. package/dist/commands/make-density/density-generator.js +180 -0
  21. package/dist/commands/make-density/index.js +139 -0
  22. package/dist/commands/make-pif/index.js +128 -11
  23. package/dist/commands/make-pif/pif-generator.js +249 -43
  24. package/dist/commands/process-utils.js +13 -2
  25. package/dist/commands/remove-track.js +20 -13
  26. package/dist/commands/set-default-session.js +35 -31
  27. package/dist/commands/shared/config-operations.js +3 -10
  28. package/dist/commands/shared/density.js +12 -0
  29. package/dist/commands/shared/sort-utils.js +67 -10
  30. package/dist/commands/shared/validators.js +38 -3
  31. package/dist/commands/sort-bed.js +2 -33
  32. package/dist/commands/sort-gff.js +2 -33
  33. package/dist/commands/text-index/adapter-utils.js +6 -23
  34. package/dist/commands/text-index/aggregate.js +40 -34
  35. package/dist/commands/text-index/command.js +35 -7
  36. package/dist/commands/text-index/config-utils.js +60 -83
  37. package/dist/commands/text-index/file-list.js +32 -18
  38. package/dist/commands/text-index/index.js +4 -4
  39. package/dist/commands/text-index/indexing-utils.js +49 -73
  40. package/dist/commands/text-index/per-track.js +44 -31
  41. package/dist/commands/text-index/validators.js +1 -1
  42. package/dist/commands/upgrade.js +20 -32
  43. package/dist/commands/validate/configManifest.generated.js +5765 -0
  44. package/dist/commands/validate/displayDefaultKeys.js +28 -0
  45. package/dist/commands/validate/index.js +97 -0
  46. package/dist/commands/validate/suggest.js +35 -0
  47. package/dist/commands/validate/types.js +6 -0
  48. package/dist/commands/validate/validateConfig.js +522 -0
  49. package/dist/index.js +104 -50
  50. package/dist/types/common.js +3 -27
  51. package/dist/utils.js +309 -128
  52. package/dist/version.js +1 -1
  53. package/package.json +8 -10
  54. package/dist/commands/make-pif/cigar-utils.js +0 -24
  55. package/dist/commands/make-pif/file-utils.js +0 -30
  56. package/dist/commands/track-utils.js +0 -74
  57. package/dist/util.js +0 -35
@@ -1,302 +1,236 @@
1
- export function makeLocationProtocol(protocol) {
2
- return (location) => {
3
- if (protocol === 'uri') {
1
+ import fs from 'node:fs';
2
+ import { adapterTypesToTrackTypeMap, formats, matchFormat, resolveIndexType, trackTypeForAdapter, } from '@jbrowse/add-track-core';
3
+ import { isURL } from '../../types/common.js';
4
+ import { densitySidecarPath } from '../shared/density.js';
5
+ function hasLocField(spec) {
6
+ return 'locField' in spec;
7
+ }
8
+ const adapterTypeToSpec = {};
9
+ for (const { spec } of formats) {
10
+ if (hasLocField(spec) && !adapterTypeToSpec[spec.adapterType]) {
11
+ adapterTypeToSpec[spec.adapterType] = spec;
12
+ }
13
+ }
14
+ // What else the sidecar with this suffix can be named. `.csi` is htslib's
15
+ // alternative for a BAI or a TBI — required for a reference over 512 Mb, and
16
+ // written on request at any size. `stripped` is the form Picard and GATK write:
17
+ // `reads.bai` beside `reads.bam`, where samtools writes `reads.bam.bai`.
18
+ //
19
+ // Per-suffix because the alternatives are: a `.crai` has no CSI form, and
20
+ // stripping the extension off `calls.vcf.gz` would name `calls.vcf.tbi`, which
21
+ // nothing writes. A `.fai`/`.gzi` has neither and so is absent here.
22
+ //
23
+ // This is `indexCandidateNames` in `@jbrowse/core/util/indexCandidates`, which
24
+ // the add-track widget and jbrowse-img share and this CLI cannot import — it
25
+ // carries no `@jbrowse/core` dependency, so `npm i -g @jbrowse/cli` stays a CLI
26
+ // rather than a copy of the app. Change one, change the other.
27
+ const ALTERNATE_SPELLINGS = {
28
+ '.bai': { csi: true, stripped: true },
29
+ '.crai': { stripped: true },
30
+ '.tbi': { csi: true },
31
+ };
32
+ /**
33
+ * The sidecar actually sitting beside `location`, or the conventional name when
34
+ * none of the spellings is there — so a missing index still reports the file
35
+ * that was expected, and `--load` still names it.
36
+ *
37
+ * Probed with `existsSync`, which is false for every candidate of a URL and so
38
+ * leaves a remote track with exactly the conventional guess it had before this
39
+ * existed. A remote `.csi` still wants `--indexFile`.
40
+ *
41
+ * The guard on the stripped spelling is load-bearing: `replace` hands back the
42
+ * subject unchanged when the pattern does not match, so `--adapterType
43
+ * BamAdapter` on a file named with no extension at all would offer the data
44
+ * file as its own index — and that file certainly exists.
45
+ */
46
+ export function siblingSidecar(location, suffix) {
47
+ const { csi, stripped } = ALTERNATE_SPELLINGS[suffix] ?? {};
48
+ const strippedName = location.replace(/\.[^./\\]+$/, suffix);
49
+ const candidates = [
50
+ `${location}${suffix}`,
51
+ ...(csi ? [`${location}.csi`] : []),
52
+ ...(stripped && strippedName !== location ? [strippedName] : []),
53
+ ];
54
+ return candidates.find(c => fs.existsSync(c)) ?? candidates[0];
55
+ }
56
+ // The feature adapters whose schema spreads
57
+ // `densityAdapterConfigSchemaFields`, so a `densityAdapter` written onto one
58
+ // resolves to a slot rather than being dropped. Change one, change the other.
59
+ const densityAdapterTypes = new Set([
60
+ 'BamAdapter',
61
+ 'CramAdapter',
62
+ 'HtsgetBamAdapter',
63
+ 'Gff3TabixAdapter',
64
+ 'GtfTabixAdapter',
65
+ 'BedTabixAdapter',
66
+ 'BigBedAdapter',
67
+ 'VcfTabixAdapter',
68
+ 'SplitVcfTabixAdapter',
69
+ ]);
70
+ /**
71
+ * The adapter with its density sidecar attached, and the local file to load
72
+ * beside the config when there is one.
73
+ *
74
+ * An explicit `--density` is taken as given, since a URL cannot be probed;
75
+ * otherwise the conventional `<file>.density.bw` counts only when it is
76
+ * actually there, the way `siblingSidecar` probes an index.
77
+ */
78
+ export function withDensityAdapter({ adapter, location, density, makeLocation, }) {
79
+ const supported = densityAdapterTypes.has(adapter.type);
80
+ if (density !== undefined && !supported) {
81
+ throw new Error(`--density has no slot on ${adapter.type}. The adapters carrying a density sidecar are: ${[...densityAdapterTypes].join(', ')}`);
82
+ }
83
+ const probed = location !== undefined && fs.existsSync(densitySidecarPath(location))
84
+ ? densitySidecarPath(location)
85
+ : undefined;
86
+ if (probed !== undefined && !supported) {
87
+ 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.`);
88
+ }
89
+ const file = density === undefined ? (supported ? probed : undefined) : density;
90
+ return file === undefined
91
+ ? { adapter }
92
+ : {
93
+ adapter: {
94
+ ...adapter,
95
+ densityAdapter: {
96
+ type: 'BigWigAdapter',
97
+ bigWigLocation: makeLocation(file),
98
+ },
99
+ },
100
+ file: isURL(file) ? undefined : file,
101
+ };
102
+ }
103
+ // builds the adapter object and the list of source files for a spec in one
104
+ // place, so the config it writes and the files add-track copies can never drift
105
+ function buildFromSpec(spec, { location, index, bed1, bed2, makeLocation }) {
106
+ switch (spec.kind) {
107
+ case 'single':
108
+ return {
109
+ adapter: {
110
+ type: spec.adapterType,
111
+ [spec.locField]: makeLocation(location),
112
+ },
113
+ files: [location],
114
+ };
115
+ case 'indexed': {
116
+ const idx = index || siblingSidecar(location, spec.suffix);
4
117
  return {
5
- uri: location,
6
- locationType: 'UriLocation',
118
+ adapter: {
119
+ type: spec.adapterType,
120
+ [spec.locField]: makeLocation(location),
121
+ index: {
122
+ location: makeLocation(idx),
123
+ // the type follows the file that was CHOSEN, not the one the user
124
+ // typed, so a detected `.csi` is opened as one
125
+ indexType: resolveIndexType(idx, spec.indexType),
126
+ },
127
+ },
128
+ files: [location, idx],
7
129
  };
8
130
  }
9
- if (protocol === 'localPath') {
131
+ case 'sidecar': {
132
+ const sidecars = spec.sidecars.map(s => ({
133
+ field: s.field,
134
+ path: s.fromIndex && index ? index : siblingSidecar(location, s.suffix),
135
+ }));
136
+ return {
137
+ adapter: {
138
+ type: spec.adapterType,
139
+ [spec.locField]: makeLocation(location),
140
+ ...Object.fromEntries(sidecars.map(s => [s.field, makeLocation(s.path)])),
141
+ },
142
+ files: [location, ...sidecars.map(s => s.path)],
143
+ };
144
+ }
145
+ case 'anchors':
10
146
  return {
11
- localPath: location,
12
- locationType: 'LocalPathLocation',
147
+ adapter: {
148
+ type: spec.adapterType,
149
+ [spec.locField]: makeLocation(location),
150
+ bed1Location: bed1 ? makeLocation(bed1) : undefined,
151
+ bed2Location: bed2 ? makeLocation(bed2) : undefined,
152
+ },
153
+ files: [location, bed1, bed2],
13
154
  };
155
+ case 'unsupported':
156
+ return { adapter: { type: 'UNSUPPORTED' }, files: [] };
157
+ }
158
+ }
159
+ export function makeLocationProtocol(protocol) {
160
+ return (location) => {
161
+ if (protocol === 'uri') {
162
+ return { uri: location, locationType: 'UriLocation' };
163
+ }
164
+ if (protocol === 'localPath') {
165
+ return { localPath: location, locationType: 'LocalPathLocation' };
14
166
  }
15
167
  throw new Error(`invalid protocol ${protocol}`);
16
168
  };
17
169
  }
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 {};
170
+ /**
171
+ * The bare filename the format table matches against — the same thing
172
+ * `@jbrowse/core`'s `getFileName` hands the guesser chain, so a path and a URL
173
+ * with a presigned query string both reduce to what the regexes expect.
174
+ */
175
+ export function fileNameOf(location) {
176
+ return (location.replaceAll('\\', '/').split('/').at(-1)?.split(/[?#]/)[0] ?? '');
81
177
  }
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
- };
178
+ // resolves the file-layout spec that both the adapter config and the copied
179
+ // file set derive from, honoring an explicit --adapterType. `typeOverride` is
180
+ // the label to stamp on the adapter when the extension's layout is reused under
181
+ // a different adapter type.
182
+ function resolveSpec(location, adapterType) {
183
+ const spec = matchFormat(fileNameOf(location))?.spec;
184
+ if (adapterType) {
185
+ const known = adapterTypeToSpec[adapterType];
186
+ if (known) {
187
+ // explicit --adapterType resolves back to its known file layout
188
+ return { spec: known };
189
+ }
190
+ else if (spec) {
191
+ // unknown adapter type on a recognized extension: reuse that extension's
192
+ // file layout (location field + any sidecar/index files) under the new
193
+ // type name, so e.g. --adapterType MyPAFAdapter on a .paf keeps its
194
+ // pafLocation instead of dropping the file. Override with --config if the
195
+ // custom adapter uses a different field.
196
+ return { spec, typeOverride: adapterType };
197
+ }
198
+ else {
199
+ // custom adapter type on an unrecognized extension: no layout to reuse
200
+ return { typeOverride: adapterType };
201
+ }
264
202
  }
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
- };
203
+ else {
204
+ return { spec };
272
205
  }
273
- return {
274
- type: 'UNKNOWN',
275
- };
276
206
  }
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';
207
+ // derives the track adapter and the raw source files together from one spec, so
208
+ // the config written and the files add-track copies can never drift. mapLocation
209
+ // turns a raw source path into the location the adapter stores (relative path +
210
+ // protocol wrapper); the returned files stay raw for the copy step.
211
+ export function guessTrack({ location, index, bed1, bed2, adapterType, mapLocation, }) {
212
+ const { spec, typeOverride } = resolveSpec(location, adapterType);
213
+ if (spec) {
214
+ const { adapter, files } = buildFromSpec(spec, {
215
+ location,
216
+ index,
217
+ bed1,
218
+ bed2,
219
+ makeLocation: mapLocation,
220
+ });
221
+ return {
222
+ adapter: typeOverride ? { ...adapter, type: typeOverride } : adapter,
223
+ files,
224
+ };
225
+ }
226
+ return { adapter: { type: typeOverride ?? 'UNKNOWN' }, files: [] };
227
+ }
228
+ export function guessTrackType(adapterType, location) {
229
+ return (trackTypeForAdapter(adapterType, location && fileNameOf(location)) ||
230
+ 'FeatureTrack');
302
231
  }
232
+ // the synteny adapters are exactly those mapping to a SyntenyTrack, so derive
233
+ // the set instead of maintaining a second hand-written list that can drift
234
+ export const syntenyAdapterTypes = new Set(Object.entries(adapterTypesToTrackTypeMap)
235
+ .filter(([, trackType]) => trackType === 'SyntenyTrack')
236
+ .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
+ }