@jbrowse/cli 4.2.1 → 5.0.0-beta.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 (54) hide show
  1. package/README.md +334 -60
  2. package/bin/run +1 -1
  3. package/bundle/index.js +493 -2555
  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 +173 -283
  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 +178 -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-pif/cigar-utils.js +62 -19
  21. package/dist/commands/make-pif/index.js +98 -11
  22. package/dist/commands/make-pif/pif-generator.js +276 -42
  23. package/dist/commands/process-utils.js +13 -2
  24. package/dist/commands/remove-track.js +20 -13
  25. package/dist/commands/set-default-session.js +35 -31
  26. package/dist/commands/shared/config-operations.js +3 -10
  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 +6 -23
  32. package/dist/commands/text-index/aggregate.js +40 -34
  33. package/dist/commands/text-index/command.js +35 -7
  34. package/dist/commands/text-index/config-utils.js +60 -83
  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 +49 -73
  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 +5394 -0
  42. package/dist/commands/validate/displayDefaultKeys.js +28 -0
  43. package/dist/commands/validate/index.js +97 -0
  44. package/dist/commands/validate/suggest.js +35 -0
  45. package/dist/commands/validate/types.js +6 -0
  46. package/dist/commands/validate/validateConfig.js +457 -0
  47. package/dist/index.js +99 -50
  48. package/dist/types/common.js +3 -27
  49. package/dist/utils.js +309 -128
  50. package/dist/version.js +1 -1
  51. package/package.json +8 -10
  52. package/dist/commands/make-pif/file-utils.js +0 -30
  53. package/dist/commands/track-utils.js +0 -74
  54. package/dist/util.js +0 -35
@@ -1,297 +1,187 @@
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
+ function hasLocField(spec) {
4
+ return 'locField' in spec;
5
+ }
6
+ const adapterTypeToSpec = {};
7
+ for (const { spec } of formats) {
8
+ if (hasLocField(spec) && !adapterTypeToSpec[spec.adapterType]) {
9
+ adapterTypeToSpec[spec.adapterType] = spec;
10
+ }
11
+ }
12
+ // What else the sidecar with this suffix can be named. `.csi` is htslib's
13
+ // alternative for a BAI or a TBI — required for a reference over 512 Mb, and
14
+ // written on request at any size. `stripped` is the form Picard and GATK write:
15
+ // `reads.bai` beside `reads.bam`, where samtools writes `reads.bam.bai`.
16
+ //
17
+ // Per-suffix because the alternatives are: a `.crai` has no CSI form, and
18
+ // stripping the extension off `calls.vcf.gz` would name `calls.vcf.tbi`, which
19
+ // nothing writes. A `.fai`/`.gzi` has neither and so is absent here.
20
+ //
21
+ // This is `indexCandidateNames` in `@jbrowse/core/util/indexCandidates`, which
22
+ // the add-track widget and jbrowse-img share and this CLI cannot import — it
23
+ // carries no `@jbrowse/core` dependency, so `npm i -g @jbrowse/cli` stays a CLI
24
+ // rather than a copy of the app. Change one, change the other.
25
+ const ALTERNATE_SPELLINGS = {
26
+ '.bai': { csi: true, stripped: true },
27
+ '.crai': { stripped: true },
28
+ '.tbi': { csi: true },
29
+ };
30
+ /**
31
+ * The sidecar actually sitting beside `location`, or the conventional name when
32
+ * none of the spellings is there — so a missing index still reports the file
33
+ * that was expected, and `--load` still names it.
34
+ *
35
+ * Probed with `existsSync`, which is false for every candidate of a URL and so
36
+ * leaves a remote track with exactly the conventional guess it had before this
37
+ * existed. A remote `.csi` still wants `--indexFile`.
38
+ *
39
+ * The guard on the stripped spelling is load-bearing: `replace` hands back the
40
+ * subject unchanged when the pattern does not match, so `--adapterType
41
+ * BamAdapter` on a file named with no extension at all would offer the data
42
+ * file as its own index — and that file certainly exists.
43
+ */
44
+ export function siblingSidecar(location, suffix) {
45
+ const { csi, stripped } = ALTERNATE_SPELLINGS[suffix] ?? {};
46
+ const strippedName = location.replace(/\.[^./\\]+$/, suffix);
47
+ const candidates = [
48
+ `${location}${suffix}`,
49
+ ...(csi ? [`${location}.csi`] : []),
50
+ ...(stripped && strippedName !== location ? [strippedName] : []),
51
+ ];
52
+ return candidates.find(c => fs.existsSync(c)) ?? candidates[0];
53
+ }
54
+ // builds the adapter object and the list of source files for a spec in one
55
+ // place, so the config it writes and the files add-track copies can never drift
56
+ function buildFromSpec(spec, { location, index, bed1, bed2, makeLocation }) {
57
+ switch (spec.kind) {
58
+ case 'single':
4
59
  return {
5
- uri: location,
6
- locationType: 'UriLocation',
60
+ adapter: {
61
+ type: spec.adapterType,
62
+ [spec.locField]: makeLocation(location),
63
+ },
64
+ files: [location],
65
+ };
66
+ case 'indexed': {
67
+ const idx = index || siblingSidecar(location, spec.suffix);
68
+ return {
69
+ adapter: {
70
+ type: spec.adapterType,
71
+ [spec.locField]: makeLocation(location),
72
+ index: {
73
+ location: makeLocation(idx),
74
+ // the type follows the file that was CHOSEN, not the one the user
75
+ // typed, so a detected `.csi` is opened as one
76
+ indexType: resolveIndexType(idx, spec.indexType),
77
+ },
78
+ },
79
+ files: [location, idx],
7
80
  };
8
81
  }
9
- if (protocol === 'localPath') {
82
+ case 'sidecar': {
83
+ const sidecars = spec.sidecars.map(s => ({
84
+ field: s.field,
85
+ path: s.fromIndex && index ? index : siblingSidecar(location, s.suffix),
86
+ }));
87
+ return {
88
+ adapter: {
89
+ type: spec.adapterType,
90
+ [spec.locField]: makeLocation(location),
91
+ ...Object.fromEntries(sidecars.map(s => [s.field, makeLocation(s.path)])),
92
+ },
93
+ files: [location, ...sidecars.map(s => s.path)],
94
+ };
95
+ }
96
+ case 'anchors':
10
97
  return {
11
- localPath: location,
12
- locationType: 'LocalPathLocation',
98
+ adapter: {
99
+ type: spec.adapterType,
100
+ [spec.locField]: makeLocation(location),
101
+ bed1Location: bed1 ? makeLocation(bed1) : undefined,
102
+ bed2Location: bed2 ? makeLocation(bed2) : undefined,
103
+ },
104
+ files: [location, bed1, bed2],
13
105
  };
106
+ case 'unsupported':
107
+ return { adapter: { type: 'UNSUPPORTED' }, files: [] };
108
+ }
109
+ }
110
+ export function makeLocationProtocol(protocol) {
111
+ return (location) => {
112
+ if (protocol === 'uri') {
113
+ return { uri: location, locationType: 'UriLocation' };
114
+ }
115
+ if (protocol === 'localPath') {
116
+ return { localPath: location, locationType: 'LocalPathLocation' };
14
117
  }
15
118
  throw new Error(`invalid protocol ${protocol}`);
16
119
  };
17
120
  }
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
- /\.pif\.b?gz$/i.test(location)) {
42
- return {
43
- file: location,
44
- index: index || `${location}.tbi`,
45
- };
46
- }
47
- else if (/\.(fa|fasta|fas|fna|mfa)$/i.test(location)) {
48
- return {
49
- file: location,
50
- index: index || `${location}.fai`,
51
- };
52
- }
53
- else if (/\.(fa|fasta|fas|fna|mfa)\.b?gz$/i.test(location)) {
54
- return {
55
- file: location,
56
- index: `${location}.fai`,
57
- index2: `${location}.gzi`,
58
- };
59
- }
60
- else if (/\.2bit$/i.test(location) ||
61
- /\.bedpe(\.gz)?$/i.test(location) ||
62
- /\/trackData.jsonz?$/i.test(location) ||
63
- /\/sparql$/i.test(location) ||
64
- /\.out(\.gz)?$/i.test(location) ||
65
- /\.paf(\.gz)?$/i.test(location) ||
66
- /\.delta(\.gz)?$/i.test(location) ||
67
- /\.bed?$/i.test(location) ||
68
- /\.(bw|bigwig)$/i.test(location) ||
69
- /\.(bb|bigbed)$/i.test(location) ||
70
- /\.vcf$/i.test(location) ||
71
- /\.gtf?$/i.test(location) ||
72
- /\.gff3?$/i.test(location) ||
73
- /\.chain(\.gz)?$/i.test(location) ||
74
- /\.hic$/i.test(location)) {
75
- return {
76
- file: location,
77
- };
78
- }
79
- return {};
121
+ /**
122
+ * The bare filename the format table matches against — the same thing
123
+ * `@jbrowse/core`'s `getFileName` hands the guesser chain, so a path and a URL
124
+ * with a presigned query string both reduce to what the regexes expect.
125
+ */
126
+ export function fileNameOf(location) {
127
+ return (location.replaceAll('\\', '/').split('/').at(-1)?.split(/[?#]/)[0] ?? '');
80
128
  }
81
- export function guessAdapter({ location, protocol, index, bed1, bed2, }) {
82
- const makeLocation = makeLocationProtocol(protocol);
83
- if (/\.bam$/i.test(location)) {
84
- return {
85
- type: 'BamAdapter',
86
- bamLocation: makeLocation(location),
87
- index: {
88
- location: makeLocation(index || `${location}.bai`),
89
- indexType: index?.toUpperCase().endsWith('CSI') ? 'CSI' : 'BAI',
90
- },
91
- };
92
- }
93
- else if (/\.cram$/i.test(location)) {
94
- return {
95
- type: 'CramAdapter',
96
- cramLocation: makeLocation(location),
97
- craiLocation: makeLocation(`${location}.crai`),
98
- };
99
- }
100
- else if (/\.gff3?$/i.test(location)) {
101
- return {
102
- type: 'Gff3Adapter',
103
- gffLocation: makeLocation(location),
104
- };
105
- }
106
- else if (/\.gff3?\.b?gz$/i.test(location)) {
107
- return {
108
- type: 'Gff3TabixAdapter',
109
- gffGzLocation: makeLocation(location),
110
- index: {
111
- location: makeLocation(index || `${location}.tbi`),
112
- indexType: index?.toUpperCase().endsWith('CSI') ? 'CSI' : 'TBI',
113
- },
114
- };
115
- }
116
- else if (/\.gtf?$/i.test(location)) {
117
- return {
118
- type: 'GtfAdapter',
119
- gtfLocation: makeLocation(location),
120
- };
121
- }
122
- else if (/\.vcf$/i.test(location)) {
123
- return {
124
- type: 'VcfAdapter',
125
- vcfLocation: makeLocation(location),
126
- };
127
- }
128
- else if (/\.vcf\.b?gz$/i.test(location)) {
129
- return {
130
- type: 'VcfTabixAdapter',
131
- vcfGzLocation: makeLocation(location),
132
- index: {
133
- location: makeLocation(index || `${location}.tbi`),
134
- indexType: index?.toUpperCase().endsWith('CSI') ? 'CSI' : 'TBI',
135
- },
136
- };
137
- }
138
- else if (/\.vcf\.idx$/i.test(location)) {
139
- return {
140
- type: 'UNSUPPORTED',
141
- };
142
- }
143
- else if (/\.bedpe(.gz)?$/i.test(location)) {
144
- return {
145
- type: 'BedpeAdapter',
146
- bedpeLocation: makeLocation(location),
147
- };
148
- }
149
- else if (/\.bed$/i.test(location)) {
150
- return {
151
- type: 'BedAdapter',
152
- bedLocation: makeLocation(location),
153
- };
154
- }
155
- else if (/\.pif\.b?gz$/i.test(location)) {
156
- return {
157
- type: 'PairwiseIndexedPAFAdapter',
158
- pifGzLocation: makeLocation(location),
159
- index: {
160
- location: makeLocation(index || `${location}.tbi`),
161
- indexType: index?.toUpperCase().endsWith('CSI') ? 'CSI' : 'TBI',
162
- },
163
- };
164
- }
165
- else if (/\.bed\.b?gz$/i.test(location)) {
166
- return {
167
- type: 'BedTabixAdapter',
168
- bedGzLocation: makeLocation(location),
169
- index: {
170
- location: makeLocation(index || `${location}.tbi`),
171
- indexType: index?.toUpperCase().endsWith('CSI') ? 'CSI' : 'TBI',
172
- },
173
- };
174
- }
175
- else if (/\.(bb|bigbed)$/i.test(location)) {
176
- return {
177
- type: 'BigBedAdapter',
178
- bigBedLocation: makeLocation(location),
179
- };
180
- }
181
- else if (/\.(bw|bigwig)$/i.test(location)) {
182
- return {
183
- type: 'BigWigAdapter',
184
- bigWigLocation: makeLocation(location),
185
- };
186
- }
187
- else if (/\.(fa|fasta|fna|mfa)$/i.test(location)) {
188
- return {
189
- type: 'IndexedFastaAdapter',
190
- fastaLocation: makeLocation(location),
191
- faiLocation: makeLocation(index || `${location}.fai`),
192
- };
193
- }
194
- else if (/\.(fa|fasta|fna|mfa)\.b?gz$/i.test(location)) {
195
- return {
196
- type: 'BgzipFastaAdapter',
197
- fastaLocation: makeLocation(location),
198
- faiLocation: makeLocation(`${location}.fai`),
199
- gziLocation: makeLocation(`${location}.gzi`),
200
- };
201
- }
202
- else if (/\.2bit$/i.test(location)) {
203
- return {
204
- type: 'TwoBitAdapter',
205
- twoBitLocation: makeLocation(location),
206
- };
207
- }
208
- else if (/\.sizes$/i.test(location)) {
209
- return {
210
- type: 'UNSUPPORTED',
211
- };
212
- }
213
- else if (/\/trackData.jsonz?$/i.test(location)) {
214
- return {
215
- type: 'NCListAdapter',
216
- rootUrlTemplate: makeLocation(location),
217
- };
218
- }
219
- else if (/\/sparql$/i.test(location)) {
220
- return {
221
- type: 'SPARQLAdapter',
222
- endpoint: location,
223
- };
224
- }
225
- else if (/\.hic$/i.test(location)) {
226
- return {
227
- type: 'HicAdapter',
228
- hicLocation: makeLocation(location),
229
- };
230
- }
231
- else if (/\.paf(.gz)?$/i.test(location)) {
232
- return {
233
- type: 'PAFAdapter',
234
- pafLocation: makeLocation(location),
235
- };
236
- }
237
- else if (/\.out(.gz)?$/i.test(location)) {
238
- return {
239
- type: 'MashMapAdapter',
240
- outLocation: makeLocation(location),
241
- };
242
- }
243
- else if (/\.chain(.gz)?$/i.test(location)) {
244
- return {
245
- type: 'ChainAdapter',
246
- chainLocation: makeLocation(location),
247
- };
248
- }
249
- else if (/\.delta(.gz)?$/i.test(location)) {
250
- return {
251
- type: 'DeltaAdapter',
252
- deltaLocation: makeLocation(location),
253
- };
254
- }
255
- else if (/\.anchors(.gz)?$/i.test(location)) {
256
- return {
257
- type: 'MCScanAnchorsAdapter',
258
- mcscanAnchorsLocation: makeLocation(location),
259
- bed1Location: bed1 ? makeLocation(bed1) : undefined,
260
- bed2Location: bed2 ? makeLocation(bed2) : undefined,
261
- };
129
+ // resolves the file-layout spec that both the adapter config and the copied
130
+ // file set derive from, honoring an explicit --adapterType. `typeOverride` is
131
+ // the label to stamp on the adapter when the extension's layout is reused under
132
+ // a different adapter type.
133
+ function resolveSpec(location, adapterType) {
134
+ const spec = matchFormat(fileNameOf(location))?.spec;
135
+ if (adapterType) {
136
+ const known = adapterTypeToSpec[adapterType];
137
+ if (known) {
138
+ // explicit --adapterType resolves back to its known file layout
139
+ return { spec: known };
140
+ }
141
+ else if (spec) {
142
+ // unknown adapter type on a recognized extension: reuse that extension's
143
+ // file layout (location field + any sidecar/index files) under the new
144
+ // type name, so e.g. --adapterType MyPAFAdapter on a .paf keeps its
145
+ // pafLocation instead of dropping the file. Override with --config if the
146
+ // custom adapter uses a different field.
147
+ return { spec, typeOverride: adapterType };
148
+ }
149
+ else {
150
+ // custom adapter type on an unrecognized extension: no layout to reuse
151
+ return { typeOverride: adapterType };
152
+ }
262
153
  }
263
- else if (/\.anchors.simple(.gz)?$/i.test(location)) {
264
- return {
265
- type: 'MCScanSimpleAnchorsAdapter',
266
- mcscanSimpleAnchorsLocation: makeLocation(location),
267
- bed1Location: bed1 ? makeLocation(bed1) : undefined,
268
- bed2Location: bed2 ? makeLocation(bed2) : undefined,
269
- };
154
+ else {
155
+ return { spec };
270
156
  }
271
- return {
272
- type: 'UNKNOWN',
273
- };
274
157
  }
275
- export const adapterTypesToTrackTypeMap = {
276
- BamAdapter: 'AlignmentsTrack',
277
- CramAdapter: 'AlignmentsTrack',
278
- BgzipFastaAdapter: 'ReferenceSequenceTrack',
279
- BigWigAdapter: 'QuantitativeTrack',
280
- IndexedFastaAdapter: 'ReferenceSequenceTrack',
281
- TwoBitAdapter: 'ReferenceSequenceTrack',
282
- VcfTabixAdapter: 'VariantTrack',
283
- VcfAdapter: 'VariantTrack',
284
- BedpeAdapter: 'VariantTrack',
285
- BedAdapter: 'FeatureTrack',
286
- HicAdapter: 'HicTrack',
287
- PAFAdapter: 'SyntenyTrack',
288
- DeltaAdapter: 'SyntenyTrack',
289
- ChainAdapter: 'SyntenyTrack',
290
- MashMapAdapter: 'SyntenyTrack',
291
- PairwiseIndexedPAFAdapter: 'SyntenyTrack',
292
- MCScanAnchorsAdapter: 'SyntenyTrack',
293
- MCScanSimpleAnchorsAdapter: 'SyntenyTrack',
294
- };
295
- export function guessTrackType(adapterType) {
296
- return adapterTypesToTrackTypeMap[adapterType] || 'FeatureTrack';
158
+ // derives the track adapter and the raw source files together from one spec, so
159
+ // the config written and the files add-track copies can never drift. mapLocation
160
+ // turns a raw source path into the location the adapter stores (relative path +
161
+ // protocol wrapper); the returned files stay raw for the copy step.
162
+ export function guessTrack({ location, index, bed1, bed2, adapterType, mapLocation, }) {
163
+ const { spec, typeOverride } = resolveSpec(location, adapterType);
164
+ if (spec) {
165
+ const { adapter, files } = buildFromSpec(spec, {
166
+ location,
167
+ index,
168
+ bed1,
169
+ bed2,
170
+ makeLocation: mapLocation,
171
+ });
172
+ return {
173
+ adapter: typeOverride ? { ...adapter, type: typeOverride } : adapter,
174
+ files,
175
+ };
176
+ }
177
+ return { adapter: { type: typeOverride ?? 'UNKNOWN' }, files: [] };
178
+ }
179
+ export function guessTrackType(adapterType, location) {
180
+ return (trackTypeForAdapter(adapterType, location && fileNameOf(location)) ||
181
+ 'FeatureTrack');
297
182
  }
183
+ // the synteny adapters are exactly those mapping to a SyntenyTrack, so derive
184
+ // the set instead of maintaining a second hand-written list that can drift
185
+ export const syntenyAdapterTypes = new Set(Object.entries(adapterTypesToTrackTypeMap)
186
+ .filter(([, trackType]) => trackType === 'SyntenyTrack')
187
+ .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
+ }