PCR/qPCR primer design for Gramene sites (SorghumBase first), as an embeddable React
component plus a headless TypeScript client. It talks to the gramene-swagger /primers
endpoints:
-
Design: Primer3 2.6.1 on a gene (± flanks), a spliced transcript (junction-spanning qPCR), a genomic region or a pasted sequence, optionally avoiding repeats.
-
Genome specificity: a Primer-BLAST-style check against the reference genome, and against its transcriptome in transcript mode.
-
Pan-genome coverage: the same check across every assembly of the species.
-
React 18 is a peer dependency; the package has no runtime dependencies.
-
ESM + CJS builds with TypeScript declarations; styles are injected at runtime and also shipped as
gramene-primers/style.css.
Status:
0.2.0. The API endpoints are under development on the gramene-swaggerprimer-designbranch. Check results follow check algorithm version 2.
npm install gramene-primers react@^18.2 react-dom@^18.2During development, link a tarball rather than npm link (a symlink would load a second React):
npm run pack:local # gramene-primers-0.2.0.tgz
cd ../gramene-search && npm install --no-save ../gramene-primers/gramene-primers-0.2.0.tgz && rm -rf .parcel-cachenpm run lint:pkg packs into a temporary directory, so it never deletes that tarball, whichever runs last.
import { PrimerDesigner } from 'gramene-primers';
<PrimerDesigner
key={gene._id}
apiBase="https://data.sorghumbase.org/sorghum_v11"
gene={gene} // Gramene gene doc; or geneId, systemName+region, sequence
systemName={gene.system_name}
modes={['gene', 'transcript', 'region', 'sequence', 'genotyping']}
state={saved} onStateChange={save} persistSequence={false}
geneHref={(id) => `?idList=${encodeURIComponent(id)}`}
/>;| Prop | Type | Default | Notes |
|---|---|---|---|
apiBase |
string |
required | Base URL including the swagger basePath, e.g. https://data.sorghumbase.org/sorghum_v11 |
client |
PrimersClient |
createPrimersClient({apiBase}) |
Supply your own (custom fetch, headers, a test double) |
gene |
GrameneGene |
Enables Gene and Transcript modes and prefills Region | |
geneId |
string |
Without a doc; fetched with GET /genes?idList= |
|
systemName |
string |
the gene's | Reference genome; enables Region mode |
region |
{region, start, end, strand?} |
the gene location | Region prefill |
sequence |
string |
Sequence-mode prefill | |
modes |
DesignerMode[] |
the four design modes | Offered modes; a mode without its inputs is hidden. genotyping is opt-in: list it to offer KASP/AS-PCR design (it needs a genome) |
genesInRegion |
GenesInRegion |
Gene models for the genotyping variant browser; without it the gene track is hidden | |
alleleFrequencies |
AlleleFrequencies |
Allele frequencies for listed variants; without it there is no frequency column. Must be the same assembly and release the primers API reads variants from | |
sequenceForRegion |
SequenceForRegion |
Reference sequence for the CAPS annotation; without it the CAPS column reads "unknown". Must be the same assembly and release the primers API reads variants from | |
enzymes |
RestrictionEnzyme[] |
COMMON_ENZYMES |
Restriction enzymes to consider — pass what the lab actually stocks |
defaultMode, defaultParams |
Initial mode and Primer3 parameter overrides | ||
state, onStateChange |
PrimerDesignerState |
uncontrolled | Controlled, JSON-serializable {v: 1, …} state |
persistSequence |
boolean |
true |
false keeps the pasted sequence out of emitted state (it survives only while mounted) |
onDesign(res, req), onCheckUpdate(job), onError(err) |
err is a PrimersApiError |
||
features |
{check, pangenome, export, map, genotyping} |
each true |
genotyping: false hides the mode even when it is listed in modes |
geneHref(id, systemName?), onGeneClick(id, systemName?) |
Gene links in results; onGeneClick handles plain left clicks, modified clicks follow geneHref |
||
geneLabel |
string |
gene name or id | Used in export file names and FASTA headers |
theme |
'light' | 'dark' | 'auto' |
'auto' |
auto follows prefers-color-scheme |
className, style |
On the .gpr-root element |
||
injectStyles |
boolean |
true |
See Styles |
poll |
{initialDelayMs, maxDelayMs, factor} |
1000, 10000, 1.5 | Check polling |
- Identity is
gene._id ?? geneId ?? systemName+region ?? hash(sequence). A change aborts the running design and polling and resets the state, unless the host passed a newstatein the same render. Hosts should also passkey. - State holds inputs, selections and
check.jobId, never responses or results.normalizeDesignerStatereads saved state tolerantly (invalid fields are dropped or clamped). - Restore:
designed: truere-runs the design once, but only when the restored inputs pass the same checks as the Design primers button. A sequence-mode state saved withpersistSequence={false}has no sequence, so nothing is sent and the form waits for a paste. - Saved check:
check.jobIdis read once. A queued or running job is polled, a finished one rendered, and a 404 shows "Results expired — Re-run check" (never an automatic resubmit). - Preview template sends
template_only. The server does not compare the product size ranges with the template length for a preview, so that rule blocks only Design primers. - Checks: results are matched to pairs by UPPERCASE primer sequence, not by rank. Check job ids are shared: when
POST /primers/checkanswers with a job that already finished (status only, no results), the job is read once withGET. - Pasted FASTA: several records are joined into one template; the Sequence input notes it (server warning
MULTIPLE_RECORDS).
- Settings: each Primer3 setting (size, Tm and GC ranges, Max Tm difference, pairs to return, product size ranges, every advanced parameter and the repeat-masking choice) has a ? button. It shows what the setting does and links to its tag in the Primer3 2.6.1 manual; one help text is open at a time.
- Inputs and explain: the interval, junction and explain texts link to the Primer3 tags they map to.
- Results:
PairsTableends with "About these columns", and the designer's results end with a credit naming the Primer3 version (engine.primer3) and its citation. - Manual links open in a new tab, so a design and a running check stay put.
Each takes theme, injectStyles, className and style (StyleProps). Used on their own they render a .gpr-root; inside a PrimerDesigner they share the designer's.
| Component | Main props |
|---|---|
PairsTable |
pairs, template?, checkedRanks? + onCheckedChange? (check boxes), selectedRank? + onSelect?, check? (a job: verdict chips), submitted?, label?, caption? |
TemplateMap |
template, pairs?, selectedRank?, onSelect?, target?, included?, excluded?, title? |
SpecificityResults |
results, block ('genome' or 'transcriptome'), pairs?, job?, submitted?, systemName?, geneHref?, onGeneClick?, maxRows? (100) |
PangenomeMatrix |
results (PangenomeResults), requestedGenomes? (pending rows), genomes? (display names), params? (results.params), pairLabels?, transcriptModelsOnly?, geneHref?, onGeneClick?, caption?; plus PangenomeLegend |
GenomePicker |
genomes, mode, systemName?, selected? (null = all), onChange(list | undefined), disabled?, label? |
Reading check results (algorithm version 2):
- Amplifying products: a product counts only when each primer has at most
max_amplifying_mismatchesmismatches (default 3) and passes the Primer-BLAST 3′ rule. Other products areunlikely. - Unlikely products are listed with
include_unlikely, and the table says why: "a primer has more than 3 mismatches" or "mismatches near the 3′ end". A pan-genome genome with only unlikely products isno_amplicon, and CellDetail names its closest product. - Approximate counts: mismatch counts taken from BLAST alignments (cDNA hits) are marked approximate.
- Pan-genome caps: a genome whose search hit a site, candidate or re-alignment cap is
truncated, and its status is a lower bound. The matrix marks that cell⋯, the column header shows "N incomplete", a note explains the marker, and CellDetail says products may have been missed. The data is insummary.truncatedand in the TSVtruncatedcolumn. - Transcript mode: the genome block has no on-target product. Products inside the gene are genomic DNA products; the rest are off-targets.
- Injection: the stylesheet is injected once per document as
<style id="gramene-primers-styles">at the start of<head>, so host rules of equal specificity win.PrimerDesignerand each standalone component inject it. Components rendered inside aPrimerDesignernever inject on their own; the designer'sinjectStylesdecides. - CSP-strict pages (no
'unsafe-inline'instyle-src): passinjectStyles={false}to every top-level component and import the file instead:import 'gramene-primers/style.css'; // dist/gramene-primers.css
ensureStylesInjected(target?)injects intodocumentor aShadowRoot. It returnstruewhen it added the element and is safe outside browsers.STYLE_ELEMENT_IDandPRIMERS_CSS(the text) are exported too.- Scoping: every rule sits under
.gpr-root, withgpr-class names and--gpr-*custom properties and no!important. A defensive reset beats the Bootstrap 4 reboot. Themes are.gpr-theme-light,.gpr-theme-darkandauto; the layout uses two columns at a container width of 960 px or more. - Resizable columns: in the two-column layout, drag the bar between the form and the results, or focus it and use the arrow keys (Shift for bigger steps), Home and End. Double-click resets the default. The form column stays at least 300 px and the results at least 360 px; the width is kept in
state.view.formWidth.
import { mount, ensureStylesInjected } from 'gramene-primers';
const handle = mount('#primers', { apiBase, geneId: 'SORBI_3001G000200', onStateChange: save });
handle.update({ theme: 'dark' }); // merges props and re-renders
handle.unmount(); // aborts design and polling requests and empties the element
// Inside a shadow root:
const shadow = host.attachShadow({ mode: 'open' });
ensureStylesInjected(shadow);
mount(shadow.appendChild(document.createElement('div')), { apiBase, gene, injectStyles: false });mount(el, props) takes an element or a selector and throws when nothing matches. React and ReactDOM 18 must still be installed, since they are peer dependencies.
import {
createPrimersClient, buildDesignRequest, buildCheckRequest, initialDesignerState,
matchCheckResults, estimateCheckCpu, pairsToTSV, isAbortError, PrimersApiError,
} from 'gramene-primers';
const client = createPrimersClient({ apiBase: 'https://data.sorghumbase.org/sorghum_v11' });
const gene = await client.getGene('SORBI_3001G000200');
const state = initialDesignerState({ gene, defaultMode: 'transcript' });
const design = await client.design(buildDesignRequest(state, { gene }));
const request = buildCheckRequest({
mode: 'transcript', systemName: gene.system_name, geneId: gene._id,
transcriptId: design.template.transcript_id, pairs: design.pairs.slice(0, 2),
});
const job = await client.runCheck(request, { onUpdate: (j) => console.log(j.status, j.progress) });
const byPair = matchCheckResults(design.pairs, job); // matched by UPPERCASE primer sequence| Method | Endpoint | Notes |
|---|---|---|
design(req, {signal}) |
POST /primers/design |
60 s timeout |
listGenomes(systemName) |
GET /primers/genomes?system_name= |
memoized per genome, evicted on error |
submitCheck(req) |
POST /primers/check |
created: true for 202, false for an existing job (200); the answer has status and progress only |
getCheck(jobId) |
GET /primers/check/{job_id} |
the job document, with request and results |
pollCheck(jobId, opts) |
resolves at done or error |
|
runCheck(req, opts) |
submit (reading an already finished job once), then poll; resubmits once if the job expires | |
getGene(geneId) |
GET /genes?idList= |
null when unknown |
Transport rules:
- Every request sends
Accept: application/jsonand usescache: 'no-store',credentials: 'omit'andmode: 'cors'. - Path and query values are URI-encoded, and no other query parameters are ever added.
Failures reject with PrimersApiError {status, code, message, details, errors, retryAfterMs}:
| Response | code |
|---|---|
Handler error {message, code, details} |
the server's code, e.g. UNKNOWN_GENE, BUSY, JOB_TOO_LARGE, INVALID_PARAMS |
Swagger validator 400 {message, errors} |
VALIDATION; errors[] is the validator's list, and flattenValidationErrors(errors) returns the nested reasons (e.g. OBJECT_ADDITIONAL_PROPERTIES, PATTERN with the body path) |
| Non-JSON body (e.g. an HTML 502) | HTTP_<status> |
| Network failure / client timeout | NETWORK / TIMEOUT (status 0) |
retryAfterMs comes from details.retry_after_s, then the Retry-After header. Aborted
requests reject with the original AbortError (test with isAbortError).
Polling starts after 1 s and grows ×1.5 per poll up to 10 s:
- It resets when
progress.donechanges and waits at least 2 s while the job is queued. - 503, other 5xx,
NETWORKandTIMEOUTerrors sleepretryAfterMs(or the current delay); polling gives up after 5 consecutive errors. - On 404 it throws
UNKNOWN_JOB, unlessresubmitis given, in which case it resubmits once. - A finished job from
submitCheck(asinitialJob, or after a resubmit) is read once withgetCheck(needsJobDocument). - It pauses while the document is hidden and stops on
signalabort.
All of these are tunable through PollOptions.
buildCheckRequest sends only the values that differ from these defaults; validateCheckParams mirrors the server.
| Param | Range | Default | Meaning |
|---|---|---|---|
max_product_size |
50–10,000 | 4000 | Largest product called; raised to cover expected products (checkMaxProductSize) |
ignore_mismatches |
3–6 | 6 | A primer site with this many mismatches is dropped |
max_amplifying_mismatches |
0–5 | 3 | A product amplifies only when each primer has at most this many; must be below ignore_mismatches. When omitted, the server lowers the default to ignore_mismatches − 1 (effectiveMaxAmplifyingMismatches) |
min_total_mismatches |
0–6 | 2 | With min_3p_mismatches: the Primer-BLAST 3′ rule for unlikely |
min_3p_mismatches |
1–5 | 2 | Mismatches required inside the 3′ window |
three_prime_window |
3–10 | 5 | Bases from the 3′ end |
include_unlikely |
boolean | false |
Also list unlikely products |
repeat_site_threshold |
1–100 | 5 | More near-perfect sites than this flags a primer repetitive |
A saved pan-genome genome list is narrowed to the genomes the check can search in its mode (a cDNA database in transcript mode). When none remain, buildCheckRequest throws CheckRequestError('NO_GENOMES').
estimateCheckCpu mirrors the server's check/cost.js and is used for display and to disable Submit above 6000 CPU-s:
cpu_s = uniq_primers × [ ref_Gb × c(5) + (transcript ? 0.15 × c(5) : 0)
+ PANGENOME_CPU_FACTOR × Σ_pan (genome_Gb | 0.15) × c(6)
+ genome_tasks × REALIGN_CPU_S_PER_PRIMER_TASK ]
c = {5: 5.2, 6: 2.2, 7: 1.2} CPU-s per primer·Gb; PANGENOME_CPU_FACTOR = 2.0; REALIGN_CPU_S_PER_PRIMER_TASK = 0.6;
genome_tasks = 1 + (transcript ? 0 : pan-genome genomes) (cDNA searches re-align from the BLAST alignment)
unknown genome sizes (reference or pan-genome) count as FALLBACK_GENOME_GB = 1 Gb
PANGENOME_CPU_FACTORaccounts for pan-genome genomes being searched by up to 8 concurrent blastn processes, which used about twice the single-thread CPU on the full sorghum panel. As a result, 20 primers against all 119 sorghum genomes exceed the limit.cpu_sis rounded up, ignoring float noise below 1e-6 as the server does.- The result also reports
realign_cpu_sandgenome_tasks.
| Export | Purpose |
|---|---|
buildDesignRequest(state, ctx) |
Mode-specific body; only params that differ from the mode's server preset |
buildCheckRequest(input) |
Pair ids P{rank+1}; expected only in gene/region modes; genomes narrowed to searchable ones and omitted when all are selected; throws CheckRequestError |
availableGenomeNames(genomes, mode, systemName), defaultPangenomeGenomes |
Genomes a pan-genome check can search |
isCheckablePrimer, pairCheckability, CHECK_LIMITS |
15–36 nt ACGT primers, ≤ 10 pairs / 20 unique primers, products ≤ 10 kb |
checkMaxProductSize(req) |
Mirrors the server raise of max_product_size to min(10000, ceil(1.2 × largest expected)) |
PRESETS, PRIMER3_DEFAULTS, CHECK_DEFAULTS, CHECK_PARAM_LIMITS |
Server presets pcr / qpcr and defaults |
validateDesignParams, validateIntervals, validateCheckParams, cleanSequenceInput |
Inline validation mirroring the server (including max_size ≤ the smallest product size, and the FASTA records count) |
EXPLAIN_HINTS, explainRows, summarizeExplain |
Primer3 2.6.1 explain labels → hints (shown on NO_PAIRS) |
pairsToTSV, primersToFasta, ampliconsToFasta, offTargetsToTSV, pangenomeToTSV |
Exports; TSV cells are protected against spreadsheet formula injection |
copyText, downloadText |
Clipboard with textarea fallback; Blob downloads |
estimateCheckCpu, REALIGN_CPU_S_PER_PRIMER_TASK |
CPU estimate (limit 6000 CPU-s) |
initialDesignerState, normalizeDesignerState, toPersistedState, designerIdentity |
Serializable v1 state and identity |
matchCheckResults, unlikelyReason, unlikelyText |
Results by sequence; why a product is unlikely |
summarizePangenome, truncatedGenomeCount, pangenomeRows, PANGENOME_STATUS_META |
Pan-genome matrix data (amplifies = single_perfect + single_mismatch + multiple; truncated is a flag, not a status) |
revcomp, transcriptLayout, cdnaToGenomicBlocks, mismatchIndexes, formatGenomic |
IUPAC-aware, case-preserving coordinates (1-based, inclusive) |
annotateVariants, capsCall, differentialSites, dcapsOpportunities, enzymeCounts |
CAPS / dCAPS annotation for a variant listing — see CAPS |
alleleShare, minorAlleleFrequency, dedupePopulations, populationCounts, variantAllele |
Allele frequency — see Allele frequency |
digestAmplicon, singleCutters, nonCutters, ampliconSeq |
Restriction sites in a predicted product — see Restriction sites on a product |
genomicToTemplatePosition, variantOnTemplate |
Genomic coordinates onto a template — the inverse of templateGenomicPosition, skipping introns and complementing alleles on a minus strand |
findSites, iupacMatcher, digestFragments, isResolvable, variantContext, verifyWindow |
The restriction-site engine underneath it |
COMMON_ENZYMES, findEnzyme, enzymeSpecificity, isSixCutter |
The bundled enzyme panel |
A variant can often be typed by digesting an ordinary PCR product instead of by allele-specific priming: CAPS when the variant itself creates or destroys a restriction site, dCAPS when a deliberate mismatch in the primer creates one.
This is an annotation — a property of the sequence — not a third assay type. A genotyping set is three oligos by definition, which CAPS does not fit, so nothing about designing or ordering sets changes. The column simply tells you whether a cheaper assay is available for a variant before you commit to KASP.
It runs entirely in the browser and needs no new endpoint, but it does need
reference sequence, which a variant listing does not carry. Supply
sequenceForRegion and the column fills in; omit it and every row reads
Unknown — never None, which would be a claim the client cannot make.
<PrimerDesigner
apiBase={apiBase}
gene={gene}
modes={['gene', 'genotyping']}
sequenceForRegion={async ({ system_name, region, start, end }, options) => {
const r = await fetch(`${ensemblRest}/sequence/region/${species}/${region}:${start}-${end}?content-type=application/json`, options);
return (await r.json()).seq;
}}
/>Headless, without the components:
import { annotateVariants, COMMON_ENZYMES } from 'gramene-primers';
const caps = annotateVariants(variants, windowSeq, windowStart);
caps.get(variant.key); // { verdict: 'caps' | 'dcaps' | 'none', sites, dcaps, unknown }Worth knowing before you trust a verdict:
- The sequence must match the variant source's release. A mismatched gene track looks wrong; mismatched sequence produces a confident, wrong enzyme call. Every window is therefore checked against the reference alleles the API reports, and one disagreement marks the whole window unknown.
- Six-cutters are ranked first. Four-cutters discriminate far more variants
but cut an amplicon too often to read on a gel, so they are reported and
ranked below.
enzymeSpecificityis what separates them —XmnI(GAANNNNTTC) is ten characters long and still a six-cutter. - dCAPS reports the opportunity, not the primer. It gives the enzyme, which side of the variant the mismatch sits on, how far away, and which base — not a designed primer, which needs the thermodynamics the server owns.
- Methylation is not modelled. Digests are computed from sequence alone, so a Dam- or Dcm-sensitive enzyme may fail on DNA this annotation calls cuttable.
- The panel is a default. Pass
enzymeswith what the lab stocks.
Recognition sequences and cut positions follow REBASE (Roberts et al., Nucleic Acids Res 43:D298, 2015).
How common an allele is decides whether a variant is worth typing: a marker
near fixation in the panel you mean to screen tells you nothing, however well
its primers score. Supply alleleFrequencies and the variant table gains a
sortable Frequency column, a Panel menu, and a Polymorphic only
toggle that hides variants whose minor allele is carried by under 5% of the
panel. Without the callback there is no column.
The library decides how much to ask for and how often; the host only makes the request. Frequencies are fetched in batches of 200 ids, two in flight, stopping after 600 variants with a note — a listing can run to thousands, and filling every row would mean minutes of requests for a table nobody reads to the end. Answers are kept by variant id, so filtering and sorting cost nothing.
<PrimerDesigner
apiBase={apiBase}
gene={gene}
modes={['gene', 'genotyping']}
alleleFrequencies={async ({ system_name, ids }, options) => {
const r = await fetch(`${ensemblRest}/variation/${system_name}?pops=1`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ ids }),
signal: options?.signal,
});
const body = await r.json();
return Object.fromEntries(
ids.filter((id) => body[id]).map((id) => [id, body[id].populations.map((p) => ({
population: p.population, allele: p.allele, frequency: p.frequency, count: p.allele_count ?? null,
}))]),
);
}}
/>Worth knowing before trusting a figure:
- Alleles must be written as the listing's
minimalblock writes them. A deletion is-there and in Ensembl alike, while its VCF form carries an anchor base (CA>C) that no frequency row will ever match. Matching onvcf.altsilently misses every indel;variantAllelepicks the right one. - Work out the minor-allele frequency rather than reading it. Ensembl
reports
MAFandminor_alleleas null for sorghum even where its own population rows carry real frequencies, sominorAlleleFrequencycomputes it. - Rows repeat. The same population and allele arrive several times over;
dedupePopulationscollapses them. - Panels barely overlap. EVA variants are called in the association panels, EMS ones only in the mutant panels, so with no panel chosen each row falls back to the widest panel that has anything to say about it and names which.
- A count matters as much as a frequency. 0.56% of 180 lines is one line;
the column shows
nbeside every figure for that reason. - About one variant in ten has no frequency reported at all, and one entered by hand has no id to look up.
Every designed pair carries a restriction map of its predicted product, under
the amplicon in the pair detail. This needs no callback at all — template.seq
comes back with every design — so it works in every mode, pasted sequence
included.
The table lists each enzyme with its recognition sequence, how many sites it has in the product, where those sites are in template coordinates, and the fragment sizes a digest would give; enzymes that cut fewest times come first, because a single cutter splitting the product into two identifiable bands is the useful one. Tick any number of enzymes — each gets its own colour, and their recognition sites are underlined in that colour in the amplicon sequence with the cut points marked. Select all and Select none act on whatever the table is listing. Beneath, the enzymes with no site at all are named — the check before adding a site to a primer end for cloning.
The same selection drives the template map, which carries its own checklist of every enzyme with a site in the template, so sites can be seen across the whole template rather than one amplicon at a time. Ticking an enzyme in either place marks it in both. The map behaves like a genome browser track:
- Counts follow the view. Zoom or pan and the count beside each enzyme is the number of its sites in view; an enzyme with none there is greyed but stays listed and tickable, since it may have sites a pan away.
- Colliding marks are bumped into lanes. Packing is done in pixels, so two sites that overlap when zoomed out separate when zoomed in, and the track grows or shrinks to fit — up to eight lanes, past which the last lane takes the overflow.
- Hover a mark for the enzyme, its location in the template and the genome,
the recognition sequence with its cut, and the bases actually there — which
differ for a degenerate site such as AccI (
GT^MKAC). - Colours match the key and are distinct. Each ticked enzyme is allocated a colour from the Okabe-Ito palette that no other ticked enzyme is using, and keeps it while it stays ticked. Up to eight are always told apart; beyond that colours repeat and the names carry the difference. An unticked enzyme has no colour, because it is drawn nowhere.
import { digestAmplicon, singleCutters, nonCutters } from 'gramene-primers';
const span = { start: pair.left.start, end: pair.right.end };
digestAmplicon(template.seq, span); // [{ enzyme, sites, cuts, fragments }, …]
singleCutters(digestAmplicon(template.seq, span)); // the readable diagnostics
nonCutters(template.seq, span); // safe to add to a primer endsites are {start, end, strand} in template coordinates and cuts are the
template positions where the top strand is severed, so both can be drawn
directly. Sizes are from sequence alone: methylation, star activity and partial
digests are not modelled.
npm install
npm run typecheck
npm test # vitest + jsdom unit and component tests
npm run build # dist/gramene-primers.{js,cjs,css}, dist/index.d.{ts,cts}
npm run lint:pkg # publint + @arethetypeswrong/cli on a tarball packed in a temp dir
npm run pack:local # build, then gramene-primers-0.2.0.tgz
npm run fixtures # contract request fixtures (below)
npm run dev # playground on :5174npm run dev serves examples/playground on http://localhost:5174 (strict port), with gramene-primers resolved to src/. From a workstation: ssh -L 5174:localhost:5174 squam.cshl.edu.
| URL parameter | Values |
|---|---|
api |
mock (default) replays real design fixtures and simulates check jobs (queued → running with partial results → done); live uses the real client against PRIMERS_API when the dev server has it (e.g. PRIMERS_API=https://data.sorghumbase.org/sorghum_v11a npm run dev, called directly), else against /sorghum_v11, proxied to PRIMERS_PROXY_TARGET (default http://localhost:50111) |
page |
gene-000200 (default), gene-000700, transcript-87700, transcript-46200, region, sequence, check-p1-p3, check-p5l, check-qpcr |
mockError |
Mock only: the first matching request fails with this code. BUSY, VALIDATION, FEATURE_DISABLED, PRIMER3_UNAVAILABLE apply to design; QUEUE_FULL, JOB_TOO_LARGE and any other code (as a 503) apply to the check |
Example: http://localhost:5174/?api=mock&page=check-p1-p3&mockError=QUEUE_FULL. The toolbar switches page and theme, toggles controlled state, resets the page, and restores the page with an expired job. The inspector shows the emitted state and the event log.
npm run fixtures(test/fixtures.gen.test.ts) writes builder output for every mode totest/fixtures/contract/requests/:design-*.jsonfiles arePOST /primers/designbodies;check-*.jsonfiles arePOST /primers/checkbodies.test/fixtures/contract/manifest.jsonlists each file with its swagger definition.- Copy the requests into gramene-swagger
test/primers/fixtures/contract/requests/(remove stale files there first).contract.test.jsvalidates them with sway;PRIMERS_CONTRACT_FIXTURES=<dir>points that test at another directory.
node scripts/capture-fixtures.mjsstores real API responses intest/fixtures/api/for component tests and the playground.- It needs
PRIMERS_IT_BASE=http://localhost:50111/sorghum_v11. - Add
--checksto include finished check jobs (BLAST on the server), or--only name[,name]for some captures. - A check submit that is not
202prints a warning and makes the script exit non-zero. This happens when the deterministic job already exists, for example afternpm run test:it. Delete the job from the dev store or pass--allow-existing. - A stale
<name>-running.jsonis removed before polling. - Each file is
{captured_at, base, method, path, request, status, headers, body}. It refuses to run against production.
- It needs
node test/components/fixtures/build-designs.mjsregeneratestest/components/fixtures/designs/*.json(real Primer3 designs verified against the genome). It needs fastaIdx (PRIMERS_FASTAIDX, defaulthttp://localhost:8888) andprimer3_core(PRIMER3_CORE), so it runs on squam only.test/fixtures/genes/andtest/fixtures/sequences/hold real SorghumBase v11 gene docs and sequence. Every coordinate intest/fixtures/samples.tswas verified against the genome.
PRIMERS_IT_BASE=http://localhost:50111/sorghum_v11 npm run test:it # live design/genomes/errors
PRIMERS_IT_BASE=… PRIMERS_IT_CHECKS=1 npm run test:it # plus one BLAST check job
PRIMERS_FASTAIDX=http://localhost:8888 npm run test:it # coordinates vs genome sequenceApache-2.0 (see LICENSE).