summaryrefslogtreecommitdiff
path: root/cli/cache/node.rs
diff options
context:
space:
mode:
Diffstat (limited to 'cli/cache/node.rs')
-rw-r--r--cli/cache/node.rs380
1 files changed, 380 insertions, 0 deletions
diff --git a/cli/cache/node.rs b/cli/cache/node.rs
new file mode 100644
index 000000000..62a8a8926
--- /dev/null
+++ b/cli/cache/node.rs
@@ -0,0 +1,380 @@
+// Copyright 2018-2022 the Deno authors. All rights reserved. MIT license.
+
+use std::path::Path;
+
+use deno_ast::CjsAnalysis;
+use deno_core::error::AnyError;
+use deno_core::parking_lot::Mutex;
+use deno_core::serde_json;
+use deno_runtime::deno_webstorage::rusqlite::params;
+use deno_runtime::deno_webstorage::rusqlite::Connection;
+use serde::Deserialize;
+use serde::Serialize;
+use std::path::PathBuf;
+use std::sync::Arc;
+
+use super::common::run_sqlite_pragma;
+use super::FastInsecureHasher;
+
+// todo(dsherret): use deno_ast::CjsAnalysisData directly when upgrading deno_ast
+// See https://github.com/denoland/deno_ast/pull/117
+#[derive(Serialize, Deserialize)]
+struct CjsAnalysisData {
+ pub exports: Vec<String>,
+ pub reexports: Vec<String>,
+}
+
+pub struct NodeAnalysisCache {
+ db_file_path: Option<PathBuf>,
+ inner: Arc<Mutex<Option<Option<NodeAnalysisCacheInner>>>>,
+}
+
+impl NodeAnalysisCache {
+ pub fn new(db_file_path: Option<PathBuf>) -> Self {
+ Self {
+ db_file_path,
+ inner: Default::default(),
+ }
+ }
+
+ pub fn compute_source_hash(text: &str) -> String {
+ FastInsecureHasher::new()
+ .write_str(text)
+ .finish()
+ .to_string()
+ }
+
+ pub fn get_cjs_analysis(
+ &self,
+ specifier: &str,
+ expected_source_hash: &str,
+ ) -> Option<CjsAnalysis> {
+ self
+ .with_inner(|inner| {
+ inner.get_cjs_analysis(specifier, expected_source_hash)
+ })
+ .flatten()
+ }
+
+ pub fn set_cjs_analysis(
+ &self,
+ specifier: &str,
+ source_hash: &str,
+ cjs_analysis: &CjsAnalysis,
+ ) {
+ self.with_inner(|inner| {
+ inner.set_cjs_analysis(specifier, source_hash, cjs_analysis)
+ });
+ }
+
+ pub fn get_esm_analysis(
+ &self,
+ specifier: &str,
+ expected_source_hash: &str,
+ ) -> Option<Vec<String>> {
+ self
+ .with_inner(|inner| {
+ inner.get_esm_analysis(specifier, expected_source_hash)
+ })
+ .flatten()
+ }
+
+ pub fn set_esm_analysis(
+ &self,
+ specifier: &str,
+ source_hash: &str,
+ top_level_decls: &Vec<String>,
+ ) {
+ self.with_inner(|inner| {
+ inner.set_esm_analysis(specifier, source_hash, top_level_decls)
+ });
+ }
+
+ fn with_inner<TResult>(
+ &self,
+ action: impl FnOnce(&NodeAnalysisCacheInner) -> Result<TResult, AnyError>,
+ ) -> Option<TResult> {
+ // lazily create the cache in order to not
+ let mut maybe_created = self.inner.lock();
+ let inner = match maybe_created.as_ref() {
+ Some(maybe_inner) => maybe_inner.as_ref(),
+ None => {
+ let maybe_inner = match NodeAnalysisCacheInner::new(
+ self.db_file_path.as_deref(),
+ crate::version::deno(),
+ ) {
+ Ok(cache) => Some(cache),
+ Err(err) => {
+ // should never error here, but if it ever does don't fail
+ if cfg!(debug_assertions) {
+ panic!("Error creating node analysis cache: {:#}", err);
+ } else {
+ log::debug!("Error creating node analysis cache: {:#}", err);
+ None
+ }
+ }
+ };
+ *maybe_created = Some(maybe_inner);
+ maybe_created.as_ref().and_then(|p| p.as_ref())
+ }
+ }?;
+ match action(inner) {
+ Ok(result) => Some(result),
+ Err(err) => {
+ // should never error here, but if it ever does don't fail
+ if cfg!(debug_assertions) {
+ panic!("Error using esm analysis: {:#}", err);
+ } else {
+ log::debug!("Error using esm analysis: {:#}", err);
+ }
+ None
+ }
+ }
+ }
+}
+
+struct NodeAnalysisCacheInner {
+ conn: Connection,
+}
+
+impl NodeAnalysisCacheInner {
+ pub fn new(
+ db_file_path: Option<&Path>,
+ version: String,
+ ) -> Result<Self, AnyError> {
+ let conn = match db_file_path {
+ Some(path) => Connection::open(path)?,
+ None => Connection::open_in_memory()?,
+ };
+ Self::from_connection(conn, version)
+ }
+
+ fn from_connection(
+ conn: Connection,
+ version: String,
+ ) -> Result<Self, AnyError> {
+ run_sqlite_pragma(&conn)?;
+ create_tables(&conn, &version)?;
+
+ Ok(Self { conn })
+ }
+
+ pub fn get_cjs_analysis(
+ &self,
+ specifier: &str,
+ expected_source_hash: &str,
+ ) -> Result<Option<CjsAnalysis>, AnyError> {
+ let query = "
+ SELECT
+ data
+ FROM
+ cjsanalysiscache
+ WHERE
+ specifier=?1
+ AND source_hash=?2
+ LIMIT 1";
+ let mut stmt = self.conn.prepare_cached(query)?;
+ let mut rows = stmt.query(params![specifier, &expected_source_hash])?;
+ if let Some(row) = rows.next()? {
+ let analysis_info: String = row.get(0)?;
+ let analysis_info: CjsAnalysisData =
+ serde_json::from_str(&analysis_info)?;
+ Ok(Some(CjsAnalysis {
+ exports: analysis_info.exports,
+ reexports: analysis_info.reexports,
+ }))
+ } else {
+ Ok(None)
+ }
+ }
+
+ pub fn set_cjs_analysis(
+ &self,
+ specifier: &str,
+ source_hash: &str,
+ cjs_analysis: &CjsAnalysis,
+ ) -> Result<(), AnyError> {
+ let sql = "
+ INSERT OR REPLACE INTO
+ cjsanalysiscache (specifier, source_hash, data)
+ VALUES
+ (?1, ?2, ?3)";
+ let mut stmt = self.conn.prepare_cached(sql)?;
+ stmt.execute(params![
+ specifier,
+ &source_hash.to_string(),
+ &serde_json::to_string(&CjsAnalysisData {
+ // temporary clones until upgrading deno_ast
+ exports: cjs_analysis.exports.clone(),
+ reexports: cjs_analysis.reexports.clone(),
+ })?,
+ ])?;
+ Ok(())
+ }
+
+ pub fn get_esm_analysis(
+ &self,
+ specifier: &str,
+ expected_source_hash: &str,
+ ) -> Result<Option<Vec<String>>, AnyError> {
+ let query = "
+ SELECT
+ data
+ FROM
+ esmglobalscache
+ WHERE
+ specifier=?1
+ AND source_hash=?2
+ LIMIT 1";
+ let mut stmt = self.conn.prepare_cached(query)?;
+ let mut rows = stmt.query(params![specifier, &expected_source_hash])?;
+ if let Some(row) = rows.next()? {
+ let top_level_decls: String = row.get(0)?;
+ let decls: Vec<String> = serde_json::from_str(&top_level_decls)?;
+ Ok(Some(decls))
+ } else {
+ Ok(None)
+ }
+ }
+
+ pub fn set_esm_analysis(
+ &self,
+ specifier: &str,
+ source_hash: &str,
+ top_level_decls: &Vec<String>,
+ ) -> Result<(), AnyError> {
+ let sql = "
+ INSERT OR REPLACE INTO
+ esmglobalscache (specifier, source_hash, data)
+ VALUES
+ (?1, ?2, ?3)";
+ let mut stmt = self.conn.prepare_cached(sql)?;
+ stmt.execute(params![
+ specifier,
+ &source_hash.to_string(),
+ &serde_json::to_string(top_level_decls)?,
+ ])?;
+ Ok(())
+ }
+}
+
+fn create_tables(conn: &Connection, cli_version: &str) -> Result<(), AnyError> {
+ // INT doesn't store up to u64, so use TEXT for source_hash
+ conn.execute(
+ "CREATE TABLE IF NOT EXISTS cjsanalysiscache (
+ specifier TEXT PRIMARY KEY,
+ source_hash TEXT NOT NULL,
+ data TEXT NOT NULL
+ )",
+ [],
+ )?;
+ conn.execute(
+ "CREATE UNIQUE INDEX IF NOT EXISTS cjsanalysiscacheidx
+ ON cjsanalysiscache(specifier)",
+ [],
+ )?;
+ conn.execute(
+ "CREATE TABLE IF NOT EXISTS esmglobalscache (
+ specifier TEXT PRIMARY KEY,
+ source_hash TEXT NOT NULL,
+ data TEXT NOT NULL
+ )",
+ [],
+ )?;
+ conn.execute(
+ "CREATE UNIQUE INDEX IF NOT EXISTS esmglobalscacheidx
+ ON esmglobalscache(specifier)",
+ [],
+ )?;
+ conn.execute(
+ "CREATE TABLE IF NOT EXISTS info (
+ key TEXT PRIMARY KEY,
+ value TEXT NOT NULL
+ )",
+ [],
+ )?;
+
+ // delete the cache when the CLI version changes
+ let data_cli_version: Option<String> = conn
+ .query_row(
+ "SELECT value FROM info WHERE key='CLI_VERSION' LIMIT 1",
+ [],
+ |row| row.get(0),
+ )
+ .ok();
+ if data_cli_version != Some(cli_version.to_string()) {
+ conn.execute("DELETE FROM cjsanalysiscache", params![])?;
+ conn.execute("DELETE FROM esmglobalscache", params![])?;
+ let mut stmt = conn
+ .prepare("INSERT OR REPLACE INTO info (key, value) VALUES (?1, ?2)")?;
+ stmt.execute(params!["CLI_VERSION", &cli_version])?;
+ }
+
+ Ok(())
+}
+
+#[cfg(test)]
+mod test {
+ use super::*;
+
+ #[test]
+ pub fn node_analysis_cache_general_use() {
+ let conn = Connection::open_in_memory().unwrap();
+ let cache =
+ NodeAnalysisCacheInner::from_connection(conn, "1.0.0".to_string())
+ .unwrap();
+
+ assert!(cache.get_cjs_analysis("file.js", "2").unwrap().is_none());
+ let cjs_analysis = CjsAnalysis {
+ exports: vec!["export1".to_string()],
+ reexports: vec!["re-export1".to_string()],
+ };
+ cache
+ .set_cjs_analysis("file.js", "2", &cjs_analysis)
+ .unwrap();
+ assert!(cache.get_cjs_analysis("file.js", "3").unwrap().is_none()); // different hash
+ let actual_cjs_analysis =
+ cache.get_cjs_analysis("file.js", "2").unwrap().unwrap();
+ assert_eq!(actual_cjs_analysis.exports, cjs_analysis.exports);
+ assert_eq!(actual_cjs_analysis.reexports, cjs_analysis.reexports);
+
+ assert!(cache.get_esm_analysis("file.js", "2").unwrap().is_none());
+ let esm_analysis = vec!["esm1".to_string()];
+ cache
+ .set_esm_analysis("file.js", "2", &esm_analysis)
+ .unwrap();
+ assert!(cache.get_esm_analysis("file.js", "3").unwrap().is_none()); // different hash
+ let actual_esm_analysis =
+ cache.get_esm_analysis("file.js", "2").unwrap().unwrap();
+ assert_eq!(actual_esm_analysis, esm_analysis);
+
+ // adding when already exists should not cause issue
+ cache
+ .set_cjs_analysis("file.js", "2", &cjs_analysis)
+ .unwrap();
+ cache
+ .set_esm_analysis("file.js", "2", &esm_analysis)
+ .unwrap();
+
+ // recreating with same cli version should still have it
+ let conn = cache.conn;
+ let cache =
+ NodeAnalysisCacheInner::from_connection(conn, "1.0.0".to_string())
+ .unwrap();
+ let actual_analysis =
+ cache.get_cjs_analysis("file.js", "2").unwrap().unwrap();
+ assert_eq!(actual_analysis.exports, cjs_analysis.exports);
+ assert_eq!(actual_analysis.reexports, cjs_analysis.reexports);
+ let actual_esm_analysis =
+ cache.get_esm_analysis("file.js", "2").unwrap().unwrap();
+ assert_eq!(actual_esm_analysis, esm_analysis);
+
+ // now changing the cli version should clear it
+ let conn = cache.conn;
+ let cache =
+ NodeAnalysisCacheInner::from_connection(conn, "2.0.0".to_string())
+ .unwrap();
+ assert!(cache.get_cjs_analysis("file.js", "2").unwrap().is_none());
+ assert!(cache.get_esm_analysis("file.js", "2").unwrap().is_none());
+ }
+}