@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.
- package/README.md +334 -60
- package/bin/run +1 -1
- package/bundle/index.js +493 -2555
- package/dist/base.js +0 -3
- package/dist/bin.js +2 -1
- package/dist/cliFetch.js +3 -0
- package/dist/commands/add-assembly/index.js +46 -19
- package/dist/commands/add-assembly/utils.js +204 -290
- package/dist/commands/add-connection.js +73 -79
- package/dist/commands/add-track-json.js +24 -12
- package/dist/commands/add-track-utils/adapter-utils.js +173 -283
- package/dist/commands/add-track-utils/file-operations.js +27 -11
- package/dist/commands/add-track-utils/multiwig.js +47 -0
- package/dist/commands/add-track-utils/track-config.js +68 -25
- package/dist/commands/add-track-utils/validators.js +36 -20
- package/dist/commands/add-track.js +178 -62
- package/dist/commands/admin-server/index.js +30 -49
- package/dist/commands/admin-server/utils.js +235 -125
- package/dist/commands/create.js +18 -34
- package/dist/commands/make-pif/cigar-utils.js +62 -19
- package/dist/commands/make-pif/index.js +98 -11
- package/dist/commands/make-pif/pif-generator.js +276 -42
- package/dist/commands/process-utils.js +13 -2
- package/dist/commands/remove-track.js +20 -13
- package/dist/commands/set-default-session.js +35 -31
- package/dist/commands/shared/config-operations.js +3 -10
- package/dist/commands/shared/sort-utils.js +67 -10
- package/dist/commands/shared/validators.js +38 -3
- package/dist/commands/sort-bed.js +2 -33
- package/dist/commands/sort-gff.js +2 -33
- package/dist/commands/text-index/adapter-utils.js +6 -23
- package/dist/commands/text-index/aggregate.js +40 -34
- package/dist/commands/text-index/command.js +35 -7
- package/dist/commands/text-index/config-utils.js +60 -83
- package/dist/commands/text-index/file-list.js +32 -18
- package/dist/commands/text-index/index.js +4 -4
- package/dist/commands/text-index/indexing-utils.js +49 -73
- package/dist/commands/text-index/per-track.js +44 -31
- package/dist/commands/text-index/validators.js +1 -1
- package/dist/commands/upgrade.js +20 -32
- package/dist/commands/validate/configManifest.generated.js +5394 -0
- package/dist/commands/validate/displayDefaultKeys.js +28 -0
- package/dist/commands/validate/index.js +97 -0
- package/dist/commands/validate/suggest.js +35 -0
- package/dist/commands/validate/types.js +6 -0
- package/dist/commands/validate/validateConfig.js +457 -0
- package/dist/index.js +99 -50
- package/dist/types/common.js +3 -27
- package/dist/utils.js +309 -128
- package/dist/version.js +1 -1
- package/package.json +8 -10
- package/dist/commands/make-pif/file-utils.js +0 -30
- package/dist/commands/track-utils.js +0 -74
- package/dist/util.js +0 -35
|
@@ -1,297 +1,187 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
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
|
-
|
|
6
|
-
|
|
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
|
-
|
|
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
|
-
|
|
12
|
-
|
|
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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
type:
|
|
103
|
-
|
|
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
|
|
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
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
};
|
|
295
|
-
|
|
296
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
}
|