function pkg_fileinfo(id::PkgId)
    origin = get(Base.pkgorigins, id, nothing)
    origin === nothing && return nothing
    cachepath = origin.cachepath
    cachepath === nothing && return nothing
    local checksum
    provides, includes_requires, required_modules = try
        ret = @static if VERSION ≥ v"1.11.0-DEV.683" # https://github.com/JuliaLang/julia/pull/49866
            io = open(cachepath, "r")
            checksum = Base.isvalid_cache_header(io)
            iszero(checksum) && (close(io); return nothing)
            provides, (_, includes_srcfiles_only, requires), required_modules, _... =
                Base.parse_cache_header(io, cachepath)
            close(io)
            provides, (includes_srcfiles_only, requires), required_modules
        else
            checksum = UInt64(0) # Buildid prior to v"1.12.0-DEV.764", and the `srcfiles_only` API does not take `io`
            Base.parse_cache_header(cachepath; srcfiles_only = true)
        end
        ret
    catch
        return nothing
    end
    includes, _ = includes_requires
    for (pkgid, buildid) in provides
        if pkgid.uuid === id.uuid && pkgid.name == id.name
            return cachepath, includes, first.(required_modules), (UInt128(checksum) << 64 | buildid)
        end
    end
    return nothing
end

"""
    update_pkgversion!(id::PkgId)

Refresh `Base.pkgorigins[id].version` from the package's `Project.toml`.

Julia caches a package's version in `Base.pkgorigins` at load time, and
(on Julia 1.12+) `pkgversion` returns that cached value. When developing a
package and bumping its version, the cached value would otherwise go stale
(issue #684). Call this after revising a package so `pkgversion` and Pkg's
version checks stay in sync with the source.
"""
function update_pkgversion!(id::PkgId)
    isdefined(Base, :pkgorigins) || return nothing
    isdefined(Base, :get_pkgversion_from_path) || return nothing
    origin = get(Base.pkgorigins, id, nothing)
    origin === nothing && return nothing
    hasproperty(origin, :version) || return nothing
    path = origin.path
    path === nothing && return nothing
    # `path` is the package entry file (e.g. `src/Foo.jl`); the version lives in
    # the `Project.toml` one directory up from `src`. Note: we deliberately avoid
    # taking `Base.require_lock` here. The load-time lock order is
    # require_lock → revise_lock (`watch_package` is a `require` callback), and we
    # hold `revise_lock` at our call sites; acquiring `require_lock` now would
    # invert that order. A bare field assignment is safe without it.
    v = Base.get_pkgversion_from_path(joinpath(dirname(path), ".."))
    # Don't clobber a known version if the project file is momentarily unreadable
    # (e.g. a path switch in flight) or has no `version` entry.
    v === nothing || (origin.version = v)
    return nothing
end

"""
    parse_pkg_files(id::PkgId)

This function gets called by `watch_package` and runs when a package is first loaded.
Its job is to organize the files and expressions defining the module so that later we can
detect and process revisions.
"""
function parse_pkg_files(id::PkgId)
    pkgdata = get!(()->PkgData(id), pkgdatas, id)
    if use_compiled_modules()
        cachefile_includes_reqs_buildid = pkg_fileinfo(id)
        if cachefile_includes_reqs_buildid !== nothing
            cachefile, includes, reqs, buildid = cachefile_includes_reqs_buildid
            pkgdata.requirements = reqs
            if isdefined(Base, :maybe_loaded_precompile) && (mod′ = Base.maybe_loaded_precompile(id, buildid); mod′ isa Module)
                root = mod′
            elseif isdefined(Base, :loaded_precompiles) && haskey(Base.loaded_precompiles, id => buildid)
                root = Base.loaded_precompiles[id => buildid]
            else
                root = Base.root_module(id)
            end
            # Transforms recorded by `include(mapexpr, ...)` calls while the package
            # loaded/precompiled (Julia ≥ 1.14), keyed by `(including_module, abspath)`.
            # `nothing` when there were none or the recording is unavailable.
            mapexprs = @static isdefined(Base, :include_mapexprs) ? (@invokelatest Base.include_mapexprs(root)) : nothing
            matched_mapexprs = mapexprs === nothing ? nothing : Set{keytype(mapexprs)}()
            for chi in includes
                mod = root
                for mpath in chi.modpath
                    mod = getglobal(mod, Symbol(mpath))::Module
                end
                fname = relpath(chi.filename, pkgdata)
                mapexpr = identity
                if mapexprs !== nothing
                    key = (mod, chi.filename)
                    if haskey(mapexprs, key)
                        mapexpr = mapexprs[key]::Function
                        push!(matched_mapexprs, key)
                    end
                end
                # For precompiled packages, we can read the source later (whenever we need it)
                # from the *.ji cachefile. Keep `chi.filename` itself: that is the exact key
                # the cache is indexed by, and reconstructing it from `fname` is unreliable
                # when path forms diverge (e.g. symlinks, see #1033).
                push!(pkgdata, fname=>FileInfo(mod, cachefile, chi.filename; mapexpr))
            end
            if mapexprs !== nothing && length(matched_mapexprs) != length(mapexprs)
                unmatched = [key for key in keys(mapexprs) if key ∉ matched_mapexprs]
                @warn "Revise could not associate the following `include(mapexpr, ...)` records of $id with tracked files; their transforms will not be applied on revision" unmatched
            end
            CodeTracking._pkgfiles[id] = pkgdata.info
            return pkgdata
        end
    end
    # Non-precompiled package(s). Here we rely on the `include` callbacks to have
    # already populated `included_files`; all we have to do is collect the relevant
    # files.
    # Pin to Revise's frozen world (issue #552); `frozen`'s runtime dispatch also reduces
    # compiler latency, since `queue_includes!` pulls in the whole parsing/expression-splitting
    # infrastructure and it's better to wait to compile it until we actually need it.
    frozen(queue_includes!, pkgdata, id)
    return pkgdata
end

# The transform recorded when `fname` was `include(mapexpr, ...)`ed into `mod`
# (Julia ≥ 1.14, `Base.include_mapexprs`), or `identity` when there was none or no
# record is available. The record lives in a binding created when the target package
# loaded — after Revise's frozen world — hence the `@invokelatest`.
function include_mapexpr_for(mod::Module, fname::AbstractString)
    @static if isdefined(Base, :include_mapexprs)
        mod === Base.__toplevel__ && return identity
        table = @invokelatest Base.include_mapexprs(mod)
        table === nothing && return identity
        return get(table, (mod, String(fname)), identity)::Function
    else
        return identity
    end
end

"""
    parentfile, included_files = modulefiles(mod::Module)

Return the `parentfile` in which `mod` was defined, as well as a list of any
other files that were `include`d to define `mod`. If this operation is unsuccessful,
`(nothing, nothing)` is returned.

All files are returned as absolute paths.
"""
function modulefiles(mod::Module)
    function keypath(filename::AbstractString)
        filename = fixpath(filename)
        return get(src_file_key, filename, filename)
    end
    @static if isdefined(Base, :moduleloc)
        parentfile = String(Base.moduleloc(mod).file)
    else
        parentfile = String(first(methods(getfield(mod, :eval))).file)
    end
    id = PkgId(mod)
    if id.name == "Base" || Base.is_stdlib(id)
        parentfile = normpath(Base.find_source_file(parentfile))
        if !startswith(parentfile, juliadir)
            parentfile = replace(parentfile, fallback_juliadir()=>juliadir)
        end
        filedata = Base._included_files
        included_files = filter(mf->mf[1] == mod, filedata)
        return keypath(parentfile), [keypath(mf[2]) for mf in included_files]
    end
    use_compiled_modules() || return nothing, nothing   # FIXME: support non-precompiled packages
    cachefile_includes_reqs_buildid = pkg_fileinfo(id)
    cachefile_includes_reqs_buildid === nothing && return nothing, nothing
    _, includes, _, _ = cachefile_includes_reqs_buildid
    included_files = filter(mf->mf.id == id, includes)
    return keypath(parentfile), [keypath(mf.filename) for mf in included_files]
end

function modulefiles_basestlibs(id)
    ret = Revise.pkg_fileinfo(id)
    cachefile, includes = ret === nothing ? (nothing, nothing) : ret[1:2]
    # `cachefile` will be nothing for Base and stdlibs that *haven't* been moved out
    cachefile === nothing && return Iterators.drop(Base._included_files, 1)  # stepping through sysimg.jl rebuilds Base, omit it
    # stdlibs that are packages
    mod = Base.loaded_modules[id]
    return map(includes) do inc
        submod = mod
        for sm in inc.modpath
            submod = getfield(submod, Symbol(sm))
        end
        return (submod, inc.filename)
    end
end
