Documentation
¶
Index ¶
Constants ¶
const LockFileName = "gofastr-plugins.json"
LockFileName is the consumer-visible lock file the eject workflow reads and writes at the root of the consumer's repo. It records exactly what was ejected, where, and what each file's bytes hashed to — the pair of hashes that lets a later `gofastr-plugin diff` tell a local edit from an upstream change without re-running the eject.
const LockVersion = "1"
LockVersion is the schema version of the lock file's SHAPE. Bump only on a breaking change to the fields below; a vendored lock from an older CLI keeps parsing under the same value.
const SourceModulePath = "github.com/DonaldMurillo/gofastr-plugins"
SourceModulePath is the import-path prefix of THIS repo. It is duplicated here (gofastrplugins also carries it) rather than imported from the root package, because internal/eject must stay decoupled from the 11 MB embed — its unit tests build a synthetic fs.FS, not the real one. The two constants are kept in sync by source_test.go's assertion that every registry modulePath sits under this prefix.
Variables ¶
This section is empty.
Functions ¶
func SaveLock ¶
SaveLock writes the lock atomically: a temp file in the same dir, then a rename, so a crash mid-write leaves the previous lock intact rather than a truncated one. Keys are sorted (encoding/json sorts map keys) and a trailing newline is appended so the file is diff-stable across runs and across machines — a re-eject that changes nothing should produce a byte-identical lock, which makes code review on a lock commit tractable.
Types ¶
type Drift ¶
type Drift int
Drift classifies how one ejected file has moved relative to its lock record since the last Apply. The five values encode the matrix of "did the local copy move" × "did upstream move" plus the absence case, which is what lets `gofastr-plugin diff` distinguish "you edited it" from "they shipped a new version" from "both" — the three cases that need different reconciliation.
const ( // DriftNone: local and upstream both unchanged since eject. DriftNone Drift = iota // DriftLocal: the user edited the file on disk (vendored hash mismatch), // upstream did not move. A re-eject without --force would conflict. DriftLocal // DriftUpstream: upstream changed (the embedded bytes differ from the // recorded upstream hash); the user has not edited their copy. A re-eject // would update cleanly. DriftUpstream // DriftBoth: both sides moved. The hardest case — a re-eject conflicts and // a force would discard the local edit. The diff output shows both sides. DriftBoth // DriftMissing: the file is gone from disk. Either the user deleted it or // the lock references a path the working tree no longer carries. DriftMissing )
type DriftEntry ¶
DriftEntry is one file's drift verdict, plus (for text files) the unified diff between what is on disk and what a re-eject would now write. The diff is empty unless drift is non-none, so a quiet tree produces quiet output.
func Compare ¶
Compare classifies drift for every file of one ejected plugin. It walks the same embedded subtree BuildPlan does (so the comparison reflects what the current embedded tree holds, not a stale snapshot), and for each file pairs:
- the lock's recorded upstream hash vs the current embedded upstream hash (did upstream move?), and
- the lock's recorded vendored hash vs the file now on disk (did the user move?).
Files the lock records but the current source no longer carries are reported as DriftUpstream too — upstream removing a file is still an upstream change a re-eject would propagate.
Options matter here for the same reason they matter in BuildPlan: WithJS and WithTests select which files are in scope, so drift is reported against the same set of files a re-eject would touch.
type File ¶
type File struct {
// Rel is the dest path relative to ProjectRoot, with forward slashes — it
// is the lock's key and stable across platforms.
Rel string
// Src is the upstream path inside the embedded tree, e.g.
// "mermaid/plugin.go" or "richtext/ssr/render.go". Carried for diagnostics.
Src string
// Status is what Apply will do with this file.
Status Status
// Content is the post-rewrite bytes to write. For non-Go files it equals
// the upstream bytes verbatim.
Content []byte
// Upstream is the pre-rewrite source bytes — the original the embedded tree
// holds. Hashed into the lock's "upstream" slot so a later diff can tell an
// upstream change from a local edit.
Upstream []byte
}
File is one file in a Plan: where it lands, where it came from, what the rewrite produced, and the status BuildPlan assigned it.
type FileHashes ¶
FileHashes carries the two hashes that make drift classification possible:
- Vendored is what we WROTE (post-rewrite). A mismatch between this and the file currently on disk means the USER edited the file — the one case Apply must refuse without --force, because clobbering it would destroy the only copy of work the user did.
- Upstream is the pre-rewrite source bytes (what the embedded tree held at eject time). A mismatch between this and the current embedded copy means UPSTREAM moved under us.
With both, `diff` can distinguish "you edited" from "they shipped a new version" from "both" — and do it without re-running the original eject.
type Lock ¶
type Lock struct {
Version string `json:"version"`
Source string `json:"source"`
Dir string `json:"dir"`
Plugins map[string]*LockPlugin `json:"plugins"`
}
Lock is the whole eject lock: the source repo each plugin came from, the parent dir vendored plugins live under, and one entry per ejected plugin.
func LoadLock ¶
LoadLock reads the lock at path. A missing lock is not an error: it returns a fresh, empty Lock so the first eject starts from a clean slate. Malformed JSON is an error; the lock is ours, but a truncated write is still possible, and silently treating it as empty would mask a half-written file.
The decoder is deliberately lenient about unknown fields: a future CLI that adds a key must still read locks written by this one (and vice-versa), so we do not impose the registry's strictness here.
type LockPlugin ¶
type LockPlugin struct {
Version string `json:"version"`
EjectedFrom string `json:"ejectedFrom"`
EjectedAt string `json:"ejectedAt"`
Dir string `json:"dir"`
WithTests bool `json:"withTests"`
WithJS bool `json:"withJS"`
Files map[string]*FileHashes `json:"files"`
}
LockPlugin is one ejected plugin's record: its version, where it landed, the options it was ejected with, and a per-file hash pair.
type Options ¶
type Options struct {
// Plugin is the registry name to eject, e.g. "mermaid" (or "map" for the
// geomap package — the registry name, not the directory).
Plugin string
// ProjectRoot is the absolute path of the consumer repo — the directory
// holding go.mod. The lock lives here and vendored files live under it.
ProjectRoot string
// DestModule is the consumer's module path, read from its go.mod. It is the
// import-path prefix the plugin's own packages rewrite to.
DestModule string
// DestDir is the repo-relative parent dir for vendored plugins, e.g.
// "internal/plugins". A plugin named mermaid lands at DestDir/mermaid/.
DestDir string
// WithTests also vendors *_test.go. Off by default: the tests pull chromedp
// into the consumer's go.mod, which is a real cost, and a consumer who
// ejects owns the code and writes their own tests against it.
WithTests bool
// WithJS also vendors the js/ TypeScript sources. On by default (the CLI
// inverts a --no-js flag into this bool): owning the prebuilt bundle means
// owning its source, otherwise a consumer who edits the bundle has no way
// to rebuild it.
WithJS bool
// Force overwrites files whose hash drifted from the recorded vendored hash
// — i.e. files the user edited. Without it, Apply refuses those rather
// than silently destroy the only copy of the user's work.
Force bool
}
Options is one eject request: which plugin, into which repo, where under it, and what to include. The CLI fills it; the engine reads it.
type Plan ¶
type Plan struct {
Plugin string // registry name
Version string // plugin's own version, from the registry row
Dir string // repo-relative dest dir, e.g. "internal/plugins/mermaid"
Files []File
}
Plan is the set of file actions for ejecting one plugin, plus the metadata Apply records in the lock. BuildPlan constructs it; Apply executes it.
func BuildPlan ¶
BuildPlan walks the embedded source for one plugin, rewrites its Go imports, and classifies each file against what is on disk and what the lock records. It does not write anything — that is Apply's job, and only after the caller (or a --dry-run human) has seen the plan.
The src FS is parameterised so unit tests can feed a synthetic tree; the CLI passes gofastrplugins.Source(). The registry row p carries the plugin's name, version, and modulePath (from which the source directory is derived).
func (*Plan) Apply ¶
Apply executes a plan: writes each file atomically, then rewrites the lock. It refuses the entire plan — not just the conflicting file — if any file is StatusConflict and Force is false. Refusing atomically is what keeps a half-applied update out of the working tree: the user runs `add`, it either lands every file or none, never "five of six and a conflict on the seventh".
type Status ¶
type Status int
Status is the per-file outcome BuildPlan classifies. The four values encode the safe-action matrix: Create and Update may proceed; Unchanged is a no-op; Conflict must stop the plan unless --force, because a file the user edited is the one thing overwriting would destroy.