chore: project skeleton — module contracts, docs plan, GPLv3
This commit is contained in:
commit
a33ed2eb30
20 changed files with 2220 additions and 0 deletions
5
src/cache/mod.rs
vendored
Normal file
5
src/cache/mod.rs
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
//! On-disk index cache.
|
||||
//!
|
||||
//! Intentionally empty in Phase 1: the project is rescanned on every run.
|
||||
//! Phase 2 will persist the import graph under `.jmove/` and support
|
||||
//! incremental reindexing (see `todo.md`).
|
||||
36
src/cli/json.rs
Normal file
36
src/cli/json.rs
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
//! JSON output shapes shared by all `--json` commands.
|
||||
//!
|
||||
//! Every response is an [`Envelope`] whose `status` is one of
|
||||
//! `"ok" | "dry_run" | "error"`, plus a machine-readable error `code`
|
||||
//! and a human `hint` on failure (see `docs/SKILL.md`).
|
||||
|
||||
use serde::Serialize;
|
||||
|
||||
/// Top-level envelope for every `--json` response.
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct Envelope<T: Serialize> {
|
||||
/// One of `"ok"`, `"dry_run"`, `"error"`.
|
||||
pub status: &'static str,
|
||||
/// The operation that produced this response, e.g. `"mv"`, `"check"`.
|
||||
pub operation: &'static str,
|
||||
/// Command-specific payload.
|
||||
#[serde(flatten)]
|
||||
pub data: T,
|
||||
}
|
||||
|
||||
/// Error payload: stable `code`, human `message`, actionable `hint`.
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct ErrorData {
|
||||
/// Machine-readable code, e.g. `TARGET_EXISTS`, `SOURCE_NOT_FOUND`.
|
||||
pub code: String,
|
||||
/// Human-readable explanation.
|
||||
pub message: String,
|
||||
/// What the caller should do next (never null in output; omit if none).
|
||||
pub hint: Option<String>,
|
||||
}
|
||||
|
||||
/// Serialize `value` as pretty JSON to stdout.
|
||||
pub fn print<T: Serialize>(value: &T) {
|
||||
let _ = value;
|
||||
todo!("cli agent: println!(serde_json::to_string_pretty)")
|
||||
}
|
||||
96
src/cli/mod.rs
Normal file
96
src/cli/mod.rs
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
//! Command line interface: argument parsing, dispatch and exit codes.
|
||||
//!
|
||||
//! Exit codes (mirrored in `docs/SKILL.md`):
|
||||
//! `0` success · `1` operation error · `2` broken imports found.
|
||||
|
||||
pub mod json;
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
use clap::{Parser, Subcommand};
|
||||
|
||||
/// jmove — move source files, keep every import intact.
|
||||
#[derive(Debug, Parser)]
|
||||
#[command(name = "jmove", version, about, long_about = None)]
|
||||
pub struct Args {
|
||||
/// Subcommand to execute.
|
||||
#[command(subcommand)]
|
||||
pub command: Command,
|
||||
/// Project root (defaults to the current directory).
|
||||
#[arg(long, global = true, default_value = ".")]
|
||||
pub root: PathBuf,
|
||||
}
|
||||
|
||||
/// Available subcommands (MVP: `mv`, `check`).
|
||||
#[derive(Debug, Subcommand)]
|
||||
pub enum Command {
|
||||
/// Move a file and rewrite all imports referencing it.
|
||||
Mv {
|
||||
/// File being moved (project-relative or inside the root).
|
||||
source: PathBuf,
|
||||
/// Destination path.
|
||||
target: PathBuf,
|
||||
/// Preview changes without touching the disk.
|
||||
#[arg(long)]
|
||||
dry_run: bool,
|
||||
/// Machine-readable JSON output (for AI agents).
|
||||
#[arg(long)]
|
||||
json: bool,
|
||||
/// Allow overwriting an existing target file.
|
||||
#[arg(long)]
|
||||
force: bool,
|
||||
},
|
||||
/// Report broken imports in the project.
|
||||
Check {
|
||||
/// Machine-readable JSON output.
|
||||
#[arg(long)]
|
||||
json: bool,
|
||||
},
|
||||
}
|
||||
|
||||
/// Process exit codes documented for humans and agents alike.
|
||||
pub mod exit {
|
||||
/// Operation completed successfully.
|
||||
pub const OK: i32 = 0;
|
||||
/// Invalid usage, rejected plan or IO failure.
|
||||
pub const ERROR: i32 = 1;
|
||||
/// `check` found at least one broken import.
|
||||
pub const BROKEN: i32 = 2;
|
||||
}
|
||||
|
||||
/// Parse arguments and run the selected command.
|
||||
/// Returns the process exit code; `Err` is reserved for unexpected failures.
|
||||
pub fn run() -> anyhow::Result<i32> {
|
||||
let args = Args::parse();
|
||||
match args.command {
|
||||
Command::Mv {
|
||||
source,
|
||||
target,
|
||||
dry_run,
|
||||
json,
|
||||
force,
|
||||
} => mv(&args.root, &source, &target, dry_run, json, force),
|
||||
Command::Check { json } => check(&args.root, json),
|
||||
}
|
||||
}
|
||||
|
||||
/// `mv` handler: build index, plan, then dry-run-print or apply.
|
||||
fn mv(
|
||||
root: &Path,
|
||||
source: &Path,
|
||||
target: &Path,
|
||||
dry_run: bool,
|
||||
json: bool,
|
||||
force: bool,
|
||||
) -> anyhow::Result<i32> {
|
||||
let _ = (root, source, target, dry_run, json, force);
|
||||
todo!("cli agent: wire mv to core::index/plan/apply")
|
||||
}
|
||||
|
||||
/// `check` handler: report imports that resolve to nothing.
|
||||
fn check(root: &Path, json: bool) -> anyhow::Result<i32> {
|
||||
let _ = (root, json);
|
||||
todo!("cli agent: wire check to core::index")
|
||||
}
|
||||
|
||||
use std::path::Path;
|
||||
35
src/core/apply.rs
Normal file
35
src/core/apply.rs
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
//! Atomic apply with rollback, plus unified-diff rendering for dry-run.
|
||||
//!
|
||||
//! Order matters: specifier rewrites are applied to importer files first
|
||||
//! (each written atomically via temp-file + rename), and the actual
|
||||
//! `source -> target` rename happens last. Any failure mid-way triggers
|
||||
//! rollback of everything already written.
|
||||
|
||||
use std::path::Path;
|
||||
|
||||
use crate::core::JmoveResult;
|
||||
use crate::core::plan::MovePlan;
|
||||
|
||||
/// Summary of a successfully applied plan.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Applied {
|
||||
/// Number of files whose imports were rewritten.
|
||||
pub files_rewritten: usize,
|
||||
/// The moved file's new project-relative path.
|
||||
pub new_path: std::path::PathBuf,
|
||||
}
|
||||
|
||||
/// Apply `plan` under `root` atomically (see module docs). Rollback is
|
||||
/// best-effort: on restore failure the error message states which files
|
||||
/// need manual recovery.
|
||||
pub fn apply(root: &Path, plan: &MovePlan) -> JmoveResult<Applied> {
|
||||
let _ = (root, plan);
|
||||
todo!("index agent: atomic apply + rollback")
|
||||
}
|
||||
|
||||
/// Render the plan as a unified diff (rewrites + file rename) for dry-run.
|
||||
#[must_use]
|
||||
pub fn render_diff(root: &Path, plan: &MovePlan) -> JmoveResult<String> {
|
||||
let _ = (root, plan);
|
||||
todo!("index agent: diff rendering via `similar`")
|
||||
}
|
||||
79
src/core/index.rs
Normal file
79
src/core/index.rs
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
//! Project indexing: gitignore-aware filesystem scan plus the import graph.
|
||||
//!
|
||||
//! Built fresh on every command (disk cache is Phase 2). `ignore::WalkBuilder`
|
||||
//! handles `.gitignore`/hidden-file rules; every indexed TS/JS source file is
|
||||
//! parsed through [`crate::parser`] and its specifiers resolved through
|
||||
//! [`crate::parser::resolve`].
|
||||
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use crate::core::JmoveResult;
|
||||
use crate::parser::ImportRecord;
|
||||
|
||||
/// Indexed source files with O(1) membership lookups.
|
||||
#[derive(Debug, Default)]
|
||||
pub struct FileSet {
|
||||
paths: HashSet<PathBuf>,
|
||||
}
|
||||
|
||||
impl FileSet {
|
||||
/// Add a normalized project-relative path; `false` if already present.
|
||||
pub fn add(&mut self, path: PathBuf) -> bool {
|
||||
self.paths.insert(path)
|
||||
}
|
||||
|
||||
/// Whether `path` is a known indexed source file.
|
||||
#[must_use]
|
||||
pub fn contains(&self, path: &Path) -> bool {
|
||||
self.paths.contains(path)
|
||||
}
|
||||
|
||||
/// All files in deterministic sorted order (stable for tests and diffs).
|
||||
#[must_use]
|
||||
pub fn sorted(&self) -> Vec<PathBuf> {
|
||||
let mut v: Vec<PathBuf> = self.paths.iter().cloned().collect();
|
||||
v.sort();
|
||||
v
|
||||
}
|
||||
}
|
||||
|
||||
/// One import occurrence plus the project file it resolves to.
|
||||
/// `target: None` means "external" — a bare package specifier or a path
|
||||
/// that does not exist in the index.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ResolvedImport {
|
||||
/// Raw record from the parser (specifier text + byte span).
|
||||
pub record: ImportRecord,
|
||||
/// Project-relative resolved file, if any.
|
||||
pub target: Option<PathBuf>,
|
||||
}
|
||||
|
||||
/// Full in-memory project index: file set and forward import edges.
|
||||
#[derive(Debug, Default)]
|
||||
pub struct Index {
|
||||
/// Absolute project root the index was built for.
|
||||
pub root: PathBuf,
|
||||
/// All indexed source files.
|
||||
pub files: FileSet,
|
||||
/// For each file, the imports it declares (in source order).
|
||||
pub imports: HashMap<PathBuf, Vec<ResolvedImport>>,
|
||||
}
|
||||
|
||||
impl Index {
|
||||
/// Scan `root`, parse every supported source file and build the graph.
|
||||
/// Unparseable files are skipped, not fatal.
|
||||
pub fn build(root: &Path) -> JmoveResult<Self> {
|
||||
let _ = root;
|
||||
todo!(
|
||||
"index agent: scan with `ignore`, parse via crate::parser, resolve via parser::resolve"
|
||||
)
|
||||
}
|
||||
|
||||
/// Reverse edge lookup: every indexed file that imports `target`.
|
||||
#[must_use]
|
||||
pub fn importers_of(&self, target: &Path) -> Vec<PathBuf> {
|
||||
let _ = target;
|
||||
todo!("index agent: reverse-edge lookup")
|
||||
}
|
||||
}
|
||||
103
src/core/mod.rs
Normal file
103
src/core/mod.rs
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
//! Core engine: project indexing, dependency graph, move planning and
|
||||
//! atomic apply with rollback.
|
||||
//!
|
||||
//! Path convention used across the crate: every `PathBuf` produced by
|
||||
//! `jmove` is **project-root-relative, normalized** (no `.`/`..` segments).
|
||||
//! Use [`normalize_rel_path`] to canonicalize paths coming from users or
|
||||
//! from OS walking.
|
||||
|
||||
pub mod apply;
|
||||
pub mod index;
|
||||
pub mod plan;
|
||||
|
||||
use std::ffi::OsStr;
|
||||
use std::io;
|
||||
use std::path::{Component, Path, PathBuf};
|
||||
|
||||
use thiserror::Error;
|
||||
|
||||
/// Crate-wide error type surfaced to the CLI layer.
|
||||
#[derive(Debug, Error)]
|
||||
pub enum JmoveError {
|
||||
/// Filesystem or IO failure.
|
||||
#[error("io error: {0}")]
|
||||
Io(#[from] io::Error),
|
||||
/// The user supplied a path that is invalid for the requested operation.
|
||||
#[error("invalid argument: {0}")]
|
||||
InvalidArgument(String),
|
||||
/// Project index is stale (a file vanished or was moved externally).
|
||||
#[error("index is stale: {0}")]
|
||||
StaleIndex(String),
|
||||
/// The planned move cannot be applied safely.
|
||||
#[error("plan rejected: {0}")]
|
||||
PlanRejected(String),
|
||||
}
|
||||
|
||||
/// Result alias used throughout the crate.
|
||||
pub type JmoveResult<T> = Result<T, JmoveError>;
|
||||
|
||||
/// Normalize a project-relative path: strip `.` segments, collapse `..`
|
||||
/// where possible and reject paths that escape the project root.
|
||||
/// Returns `None` if the result would be empty, absolute or above the root.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use std::path::Path;
|
||||
/// use jmove::core::normalize_rel_path;
|
||||
///
|
||||
/// assert_eq!(
|
||||
/// normalize_rel_path(Path::new("./src/../utils/foo.ts")),
|
||||
/// Some(PathBuf::from("utils/foo.ts"))
|
||||
/// );
|
||||
/// assert_eq!(normalize_rel_path(Path::new("../outside")), None);
|
||||
/// ```
|
||||
#[must_use]
|
||||
pub fn normalize_rel_path(path: &Path) -> Option<PathBuf> {
|
||||
let mut stack: Vec<&OsStr> = Vec::new();
|
||||
for comp in path.components() {
|
||||
match comp {
|
||||
Component::CurDir => {}
|
||||
Component::ParentDir => {
|
||||
if stack.pop().is_none() {
|
||||
return None; // would escape the project root
|
||||
}
|
||||
}
|
||||
Component::Normal(piece) => stack.push(piece),
|
||||
// Absolute paths and Windows prefixes are not project-relative.
|
||||
Component::RootDir | Component::Prefix(_) => return None,
|
||||
}
|
||||
}
|
||||
(!stack.is_empty()).then(|| stack.iter().collect::<PathBuf>())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::normalize_rel_path;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
#[test]
|
||||
fn normalizes_dots_and_parent_dirs() {
|
||||
assert_eq!(
|
||||
normalize_rel_path(Path::new("./src/../utils/foo.ts")),
|
||||
Some(PathBuf::from("utils/foo.ts"))
|
||||
);
|
||||
assert_eq!(
|
||||
normalize_rel_path(Path::new("a/b/c/../../d.ts")),
|
||||
Some(PathBuf::from("a/d.ts"))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_escaping_and_empty_paths() {
|
||||
assert_eq!(normalize_rel_path(Path::new("../outside")), None);
|
||||
assert_eq!(normalize_rel_path(Path::new("a/../../outside")), None);
|
||||
assert_eq!(normalize_rel_path(Path::new("")), None);
|
||||
assert_eq!(normalize_rel_path(Path::new("./")), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_absolute_paths() {
|
||||
assert_eq!(normalize_rel_path(Path::new("/etc/passwd")), None);
|
||||
}
|
||||
}
|
||||
46
src/core/plan.rs
Normal file
46
src/core/plan.rs
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
//! Move planning: decide which import specifiers must be rewritten.
|
||||
//!
|
||||
//! A plan is pure data (no disk writes), so dry-run and `--json` can render
|
||||
//! it without touching the filesystem.
|
||||
|
||||
use std::ops::Range;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use crate::core::JmoveResult;
|
||||
use crate::core::index::Index;
|
||||
|
||||
/// One in-file replacement of an import specifier. Only the specifier text
|
||||
/// between the quotes is touched — the statement layout is never reformatted.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Rewrite {
|
||||
/// Project-relative file to modify.
|
||||
pub file: PathBuf,
|
||||
/// Byte range of the old specifier text (without quotes).
|
||||
pub span: Range<usize>,
|
||||
/// Specifier as currently written.
|
||||
pub old_text: String,
|
||||
/// Specifier after the move.
|
||||
pub new_text: String,
|
||||
}
|
||||
|
||||
/// Complete plan for moving `source` to `target`.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct MovePlan {
|
||||
/// Project-relative path being moved.
|
||||
pub source: PathBuf,
|
||||
/// Project-relative destination path.
|
||||
pub target: PathBuf,
|
||||
/// Specifier rewrites, grouped per importer file.
|
||||
pub rewrites: Vec<Rewrite>,
|
||||
}
|
||||
|
||||
/// Compute the rewrite plan for `source -> target`.
|
||||
///
|
||||
/// Every indexed import whose resolved target is `source` gets a new
|
||||
/// relative specifier computed from the *importer's* directory to `target`.
|
||||
/// Rewrites whose result equals the old specifier are dropped.
|
||||
pub fn plan_move(index: &Index, source: &Path, target: &Path) -> JmoveResult<MovePlan> {
|
||||
let _ = (index, source, target);
|
||||
todo!("index agent: implement planner incl. relative-specifier math")
|
||||
}
|
||||
17
src/lib.rs
Normal file
17
src/lib.rs
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
//! `jmove` — a project-aware file mover for TypeScript/JavaScript.
|
||||
//!
|
||||
//! Moving a file inside a project invalidates every relative import that
|
||||
//! points at it. `jmove` indexes the project's import graph, computes the
|
||||
//! minimal set of specifier rewrites, and applies everything atomically
|
||||
//! (with rollback), optionally in dry-run mode.
|
||||
//!
|
||||
//! Module map:
|
||||
//! - [`cli`] — argument parsing, command dispatch, user-facing output.
|
||||
//! - [`core`] — indexing, dependency graph, move planning, atomic apply.
|
||||
//! - [`parser`] — language frontends (import extraction, path resolution).
|
||||
//! - [`cache`] — on-disk index cache (Phase 2, intentionally empty for now).
|
||||
|
||||
pub mod cache;
|
||||
pub mod cli;
|
||||
pub mod core;
|
||||
pub mod parser;
|
||||
19
src/main.rs
Normal file
19
src/main.rs
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
//! Binary entry point for `jmove`.
|
||||
//!
|
||||
//! All logic lives in the library crate; this main only maps the outcome of
|
||||
//! [`jmove::cli::run`] onto process exit codes:
|
||||
//! - `0` — success,
|
||||
//! - `1` — user-facing plan/validation error (already printed),
|
||||
//! - `2` — unexpected failure.
|
||||
|
||||
/// Parses arguments, runs the selected command and converts failures into a
|
||||
/// non-zero exit code, printing a one-line message for the user.
|
||||
fn main() {
|
||||
match jmove::cli::run() {
|
||||
Ok(code) => std::process::exit(code),
|
||||
Err(err) => {
|
||||
eprintln!("jmove: {err}");
|
||||
std::process::exit(2);
|
||||
}
|
||||
}
|
||||
}
|
||||
83
src/parser/mod.rs
Normal file
83
src/parser/mod.rs
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
//! Language frontends: import extraction and module specifier resolution.
|
||||
//!
|
||||
//! ## Contract (stable across submodules — implementers must not change it)
|
||||
//!
|
||||
//! - A [`Language`] parses one source file into [`ImportRecord`]s: every
|
||||
//! *static-ish* module reference (TS `import`/`export from`/`require`/
|
||||
//! dynamic `import()`).
|
||||
//! - [`crate::core::parser_support::resolve`] turns a specifier into a
|
||||
//! project-relative file path using a resolver aware of the indexed file
|
||||
//! set. Non-project (package/bare) specifiers resolve to `None`.
|
||||
//! - Rewrites must touch **only the specifier string**, never the rest of
|
||||
//! the statement (KISS + no formatter dependency): that is why
|
||||
//! [`ImportRecord::span`] is a byte range into the original source.
|
||||
|
||||
use std::path::Path;
|
||||
|
||||
pub mod resolve;
|
||||
pub mod ts;
|
||||
|
||||
/// Source languages `jmove` understands (Phase 1: TypeScript/JavaScript).
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub enum SourceLanguage {
|
||||
/// `.ts` (non-TSX) sources.
|
||||
TypeScript,
|
||||
/// `.tsx` / `.jsx` sources.
|
||||
Tsx,
|
||||
/// Plain `.js` / `.mjs` / `.cjs` sources.
|
||||
JavaScript,
|
||||
}
|
||||
|
||||
impl SourceLanguage {
|
||||
/// Map a file extension (lowercase, no dot) to a language, if supported.
|
||||
#[must_use]
|
||||
pub fn from_extension(ext: &str) -> Option<Self> {
|
||||
match ext {
|
||||
"ts" | "mts" | "cts" => Some(Self::TypeScript),
|
||||
"tsx" | "jsx" => Some(Self::Tsx),
|
||||
"js" | "mjs" | "cjs" => Some(Self::JavaScript),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Detect the language from a file name. `None` means "not a source file
|
||||
/// we index" (skip it).
|
||||
#[must_use]
|
||||
pub fn for_path(path: &Path) -> Option<Self> {
|
||||
Self::from_extension(
|
||||
path.extension()
|
||||
.and_then(|e| e.to_str())
|
||||
.map(str::to_ascii_lowercase)
|
||||
.as_deref()?,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// One module reference found in a source file.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct ImportRecord {
|
||||
/// Raw specifier text as written, e.g. `"../utils/format"`.
|
||||
pub specifier: String,
|
||||
/// Byte range of the *specifier string contents* (inside the quotes,
|
||||
/// without the quote characters) in the parsed file. The rewriter
|
||||
/// replaces exactly this span and nothing else.
|
||||
pub span: std::ops::Range<usize>,
|
||||
/// `true` for dynamic `import("...")` / `require("...")` occurrences.
|
||||
pub is_dynamic: bool,
|
||||
}
|
||||
|
||||
/// A language frontend that extracts imports from source text.
|
||||
pub trait Language: Send + Sync {
|
||||
/// The language this frontend handles.
|
||||
fn language(&self) -> SourceLanguage;
|
||||
|
||||
/// Extract all import records from `source` in byte-offset order.
|
||||
/// Parse errors must not be fatal: return what was understood.
|
||||
fn extract_imports(&self, source: &str) -> Vec<ImportRecord>;
|
||||
}
|
||||
|
||||
/// Build the default frontend for `lang`.
|
||||
#[must_use]
|
||||
pub fn frontend_for(lang: SourceLanguage) -> Box<dyn Language> {
|
||||
Box::new(ts::TreeSitterTs::new(lang))
|
||||
}
|
||||
26
src/parser/resolve.rs
Normal file
26
src/parser/resolve.rs
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
//! Module specifier resolution for TS/JS projects.
|
||||
//!
|
||||
//! CONTRACT: see [`crate::parser`]. Given a *relative* specifier and the
|
||||
//! importing file, find which indexed project file it refers to. Bare /
|
||||
//! package specifiers (not starting with `.`) are out of project scope and
|
||||
//! resolve to `None`.
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use crate::core::index::FileSet;
|
||||
|
||||
/// Resolve `specifier` (e.g. `"../utils/fmt"`) written in the file at
|
||||
/// `importer` (project-relative), against the indexed `files`.
|
||||
///
|
||||
/// Resolution order for extensionless specifiers (Node/TS classic):
|
||||
/// 1. exact path if indexed (e.g. `"./a.ts"`),
|
||||
/// 2. `<base>` + each supported extension (`.ts`, `.tsx`, `.js`, `.jsx`,
|
||||
/// `.mjs`, `.cjs` — declaration files only when nothing else matches),
|
||||
/// 3. `<base>/index.<ext>`.
|
||||
///
|
||||
/// Returns `None` for bare specifiers or unresolvable paths.
|
||||
#[must_use]
|
||||
pub fn resolve_module(importer: &Path, specifier: &str, files: &FileSet) -> Option<PathBuf> {
|
||||
let _ = (importer, specifier, files);
|
||||
todo!("parser agent: implement resolver")
|
||||
}
|
||||
29
src/parser/ts.rs
Normal file
29
src/parser/ts.rs
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
//! Tree-sitter based frontend for TypeScript/JavaScript.
|
||||
//!
|
||||
//! CONTRACT: see [`crate::parser`]. Implement `extract_imports` using
|
||||
//! `tree-sitter-typescript` grammars.
|
||||
|
||||
use super::{ImportRecord, Language, SourceLanguage};
|
||||
|
||||
/// Frontend backed by the tree-sitter TypeScript/TSX/JS grammar.
|
||||
pub struct TreeSitterTs {
|
||||
lang: SourceLanguage,
|
||||
}
|
||||
|
||||
impl TreeSitterTs {
|
||||
/// Create a frontend for the given language variant.
|
||||
#[must_use]
|
||||
pub fn new(lang: SourceLanguage) -> Self {
|
||||
Self { lang }
|
||||
}
|
||||
}
|
||||
|
||||
impl Language for TreeSitterTs {
|
||||
fn language(&self) -> SourceLanguage {
|
||||
self.lang
|
||||
}
|
||||
|
||||
fn extract_imports(&self, _source: &str) -> Vec<ImportRecord> {
|
||||
todo!("parser agent: implement tree-sitter extraction")
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue