Documentation
¶
Overview ¶
Package preflight holds the CLI's local, before-any-network-write checks and the engine that runs them.
The ordering principle the whole package exists to serve: LOCAL TRUTHS BEFORE GLOBAL STATE. A project that cannot deploy makes zero network calls, so nothing here reaches the network, writes a file, or executes any of the user's code.
IT READS FILES UNDER THE PROJECT DIRECTORY, WITH ONE NAMED EXCEPTION, and the exception is written down here because the sentence above used to end "and nothing else". The lockfile check stats a handful of names in each ANCESTOR of the project directory, up to the filesystem root, to tell a package inside a monorepo from a standalone project with no lockfile at all. It only reads, it follows no link out of the tree, it stops at the root, and no verdict depends on what it finds — it chooses which of two messages a person is shown, because the standing advice, create a lockfile, is actively wrong for somebody whose lockfile exists three directories up.
The RESULT MODEL these checks emit lives in its own leaf package, not here, because the file walk and the check that reads the walk's output would otherwise import each other through it. The filesystem seam below stays here: only the result model moved.
Index ¶
Constants ¶
const ( DefaultSourceDir = "src" DefaultPagesDir = "src/pages" )
DefaultSourceDir and DefaultPagesDir are where Astro looks for the project's source and its pages when the config does not move them.
const DefaultPublicDir = "public"
DefaultPublicDir is where Astro looks for the files it copies into the built site unchanged, when the config does not say otherwise.
Variables ¶
This section is empty.
Functions ¶
func Run ¶
Run executes every check and returns what they found, together with a manifest of what was asked.
ALL CHECKS RUN. There is no short-circuit on the first hard failure, and that is the aggregate behaviour the whole engine exists for: a project missing astro AND a lockfile should learn both facts in one run rather than discover the second only after fixing the first.
The engine does not decide what blocking MEANS. It produces findings and a manifest; the caller renders them, and the two surfaces this program has answer a warning differently — one asks a person, the other attaches it to a result an agent reads. Putting that decision inside a check would make the second surface a rewrite rather than a renderer.
THE ROOT IS STATTED FIRST, and every check is reported as not run when it is unusable. Handed a directory that is not there, this used to run every check against it and let each draw its own conclusion — which for the config check is a confident advisory that the pages directory is missing, under a manifest saying everything ran. That is the worst available answer: nothing was wrong with the project, the caller was pointed at the wrong place, and the report described a project that does not exist as though it had been read.
Nothing here reaches the network, writes a file, or executes any of the user's code. That is a promise about THIS function; a check registered by a caller is the caller's code and the engine cannot sandbox it.
Types ¶
type Check ¶
Check is one pre-flight check as the engine sees it: the ids it reports on, and the function that does the reading.
IDs IS A LIST BECAUSE ONE CHECK CAN COVER MORE THAN ONE ID. The astro.config check answers two questions off a single parse — where the pages live, and whether the build format will produce URLs this platform can route — and re-reading the file to answer them separately would buy nothing. The manifest still needs a row for each, or a renderer could never say whether the second one was looked at.
func AstroConfigCheck ¶
func AstroConfigCheck() Check
CheckAstroConfig is the pages-directory pre-flight check, config-aware. It runs two independent concerns off of at most one file read:
- pages-dir runs only when <root>/src/pages is established to be absent — a successful stat saying so, not merely a stat that failed; the third state ("couldn't look") gets its own message and stops there, because resolving a custom srcDir would be answering a question nobody has shown needs asking. It tries to resolve a custom srcDir out of astro.config and warns when the resolved directory has no pages/ subdirectory either. It never hard-stops, on any outcome: a real Astro build with no pages directory does not fail — Astro warns and completes, emitting an empty output directory — so the one justification a hard stop here could have (a doomed build) does not hold. An integration that injects its own routes ships a perfectly working project with no pages directory anywhere, and a hard stop would have refused every one of them on the same wrong premise. Whether a project actually ends up with zero published pages is a fact only the build's own output can establish, not a pre-flight guess, so catching that belongs downstream of the build rather than in this check. And a scanner reporting no srcDir key is not the same fact as "the default applies" — it may mean the file uses a shape this scan doesn't parse, so it is treated exactly like every other can't-resolve outcome, never as grounds to assume the default.
- build-format runs on every project, because it controls whether a built page lands at .../index.html or as a flat .html file, and only one of those shapes is reachable once published. It can only warn, never hard stop, and its default is SILENCE where pages-dir's is a warning: here "cannot tell" overwhelmingly means the default applies, and warning on every config this check cannot fully parse would be noise on projects that are otherwise fine — which is not a hypothetical, it was measured. Exactly one state breaks that silence, a duplicate key; a file the scan could not finish reading is RECORDED at SeverityNote and shown by nothing. See buildFormatFinding for the ruling that changed and what changed it.
Both concerns are read from the same parse of the same file, so a project whose pages directory is exactly where Astro expects it never has its config opened more than once, and a project with no config at all never has one opened.
TWO further notes are attached independently of either concern, both about WHICH file Astro would load rather than about what is in one: that more than one candidate config exists on disk (Astro's resolution order picks the first, which is exactly the kind of thing that surprises people), and that a candidate could not be checked for at all (in which case the file this check read may not be the file Astro loads). Both attach to a pages-dir finding if one exists and stand alone if none does — the ambiguity note used to be decided inside the pages-dir path, and was therefore silently dropped on every project whose pages directory was already where Astro expects it. AstroConfigCheck is the pages-dir and build-format rows as the engine registers them — one check answering two questions off one parse of one file.
IT LIVES IN PRODUCTION RATHER THAN IN A TEST, which is a correction. This value existed only as a helper inside the engine's own suite, so the single piece of wiring that connects the finished check to the finished engine was a thing no shipped binary could reach — the suite proved the seam fits and nothing proved anybody had used it.
NEITHER ID EVER HARD-STOPS. A missing pages directory does not fail an Astro build, and an integration that injects its own routes ships without one, so refusing a deploy here would be wrong more often than right. Both rows warn, and one of them declines when the config builds its value at run time.
func AstroDepCheck ¶
func AstroDepCheck() Check
AstroDepCheck is the astro-dep row as the engine registers it.
It is a constructor rather than an exported variable so that the registration cannot be reordered or mutated by a caller holding it — every caller gets its own value, and the ids it claims are decided here rather than at the call site.
func LocalhostCheck ¶
LocalhostCheck is the localhost row as the engine registers it, over the file list it is given.
THE LIST IS A PARAMETER BECAUSE THE ENGINE'S SEAM DOES NOT CARRY ONE. A check is handed a filesystem and a root and nothing else, and the walk that produces the canonical file list deliberately sits outside the engine — so the list reaches this check by being closed over at registration. Scanning the walk's output rather than the raw tree is what keeps the dependency directory, the build output and everything the project's own ignore rules exclude out of the scan; those are where a hit would be both slow to find and irrelevant.
It takes plain strings rather than the walk's own file type so that this package does not import the walk. There is no cycle either way; what a shared type would cost is the property that neither producer of findings owns the other — and a test can then drive this check from a literal slice instead of arranging a real traversal.
The paths are project-relative and slash-separated on every platform, as the walk emits them. They are joined back into host form before anything touches the filesystem: the matrix this ships on includes Windows, where a slash-separated path is not what the operating system is being asked about.
func LockfileCheck ¶
func LockfileCheck() Check
LockfileCheck is the lockfile row as the engine registers it.
type FS ¶
type FS interface {
// Stat reports whether name exists and, when it does, its size and
// mode, FOLLOWING a symlink to whatever it points at. Used for
// existence checks (a pages directory) and the size gate below — and,
// by isFile, to classify what a symlinked candidate config actually
// points at. Never counted as "opening" a file's contents.
Stat(name string) (fs.FileInfo, error)
// Lstat reports on name itself, WITHOUT following a symlink. isFile
// calls this FIRST, so a FIFO, socket or device node named directly
// (not through a link) is rejected without ever resolving anything —
// the operation that matters for the FIFO problem is Open, not the
// stat that precedes it, so Lstat alone is enough to keep a direct
// FIFO from ever reaching Open. When Lstat reports a symlink, isFile
// goes on to Stat the same name — see isFile's own doc comment for
// why a symlink is resolved rather than refused outright.
Lstat(name string) (fs.FileInfo, error)
// Open opens name for reading its contents. This is the operation a
// read-counting wrapper counts: the common project (a pages
// directory already present, no config at all) calls this zero
// times, and every path that reads a config calls it exactly once.
Open(name string) (io.ReadCloser, error)
}
FS is the narrow filesystem surface this check needs. Production code uses OSFileSystem; tests substitute a counting wrapper so the "opened at most once" and "never opened" properties have something to measure rather than something to assert on trust.
type Folders ¶ added in v0.1.7
type Folders struct {
// Public is the folder Astro copies unchanged, project-relative and
// slash-separated. "." means the project root itself.
Public string
// Source is the folder the build compiles: srcDir.
Source string
// Pages is the folder whose files become routes: `pages` inside the
// source folder.
Pages string
// Results carries the warning when the configured public folder
// could not be read and Public is the default instead. It claims no
// check: the file-name check owns the id, and this is that check
// saying where it looked.
Results check.Results
}
Folders are where the names that reach the built site live, and what the read that found them has to say.
THEY EXIST BECAUSE THE FILE-NAME CHECK HAD BEEN ASKING THE WRONG TREE. The platform's rule about which characters a name may carry is a rule about the files it stores and serves, which are the build's OUTPUT. Two folders put names there. Astro copies the public folder into the output byte for byte. And a page's route keeps every character of its name outside the brackets, so `café.astro` is served at `café/`. Everything else is compiled away. A page called `[slug].astro` is Astro's documented way of writing a dynamic route, and checking every source file refused every project with a blog.
func ResolveFolders ¶ added in v0.1.7
ResolveFolders reads publicDir and srcDir out of the project's astro.config, with public/ and src/pages as the defaults.
A PUBLIC FOLDER IT CANNOT READ FALLS BACK TO THE DEFAULT AND SAYS SO, and never refuses. Not knowing where the public folder is costs, at worst, a name refused after the build rather than before it; refusing the deploy over it would make a working project undeployable because of how its config is written.
AND IT SAYS SO ONLY WHEN THE CONFIG NAMES publicDir. A config whose scan gave up for another reason — a template literal, a spread — and never mentions the key gets the default in silence, because that is what Astro uses for it. Warning there would repeat a measured mistake: a check that said "I could not confirm this" on most real configs, where nothing was wrong. What that leaves silent is a publicDir set inside a spread from another file, which a scan of this one file cannot see by construction.
THE SOURCE FOLDER FALLS BACK IN SILENCE. The pages-dir check already says everything there is to say about a srcDir it cannot read, in the same report; saying it twice is noise.
type OSFileSystem ¶
type OSFileSystem struct{}
OSFileSystem is the production FS: the real filesystem, through the standard library.
func (OSFileSystem) Lstat ¶
func (OSFileSystem) Lstat(name string) (fs.FileInfo, error)
Lstat implements FS.
func (OSFileSystem) Open ¶
func (OSFileSystem) Open(name string) (io.ReadCloser, error)
Open implements FS.
type Result ¶
type Result struct {
Findings []check.Finding
// Declined records, PER ID, that this check did not answer that
// question — with why, and with what kind.
//
// Per id rather than per check, because a check may cover more than
// one question off one read and may answer one of them. The config
// check answers where the pages live and gives up on the build
// format whenever the value is built at run time; a whole-check flag
// reported that give-up as a tick, on the very check the manifest
// was invented for.
//
// An id absent from this map was ANSWERED. The reason is written for
// a person and reaches a surface verbatim: a check that cannot read
// package.json says so, rather than staying quiet and letting
// silence be read as approval.
Declined map[string]check.Decline
}
Result is what one check reports back to the engine.
A check emits a Finding only when it has something to say — there is no "pass" finding — so a check that looked and liked what it saw returns a zero Result. What that shape cannot express on its own is the difference between FOUND NOTHING and NEVER LOOKED, and NotRun is where that difference is paid for.
func CheckAstroConfig ¶
func CheckAstroDep ¶
CheckAstroDep answers the cheapest and most fundamental question this program asks: is this an Astro project at all?
EVERY OUTCOME IS A HARD STOP OR SILENCE. There is no middle answer, because there is no half-deployable state here: either the project declares astro or the build has nothing to build. That makes the words matter more than usual — a hard stop is a dead end, and the reader is often somebody deploying their first site — so all four failures carry their own What, Why and Next rather than a one-line summary the renderer has to dress up.
The copy lives HERE, beside the conditions that produce it, and that is a correction to where it used to live. Three of these sentences sat in the terminal package with nothing but their own tests reading them, because a finding could not carry copy and there was no way to deliver them. Words and condition are one thing; keeping them in two packages is how they come to disagree.
func CheckLockfile ¶
CheckLockfile answers whether this project can be installed from a lockfile, and — when it cannot — which of three different things went wrong.
IT DECLINES RATHER THAN FAILS WHEN THERE IS NO package.json. "Commit a lockfile" is bad advice for a directory that is not a Node project at all: the astro-dep check has already said the useful thing, and a second, wronger instruction underneath it only muddies the fix. The decline is ENVIRONMENTAL — something outside the check stopped it looking, the reader can see it, and it joins the question the surface asks. That price is unobservable today, because the same condition hard-stops astro-dep in the same run and a hard stop never prompts; it is chosen here on purpose rather than discovered later to have been chosen by default.