Skip to main content

vivacity_core/
pest_plugin.rs

1//! Emulation of the `pestphp/pest-plugin` Composer plugin
2//! (docs/reference/plugins/pest-plugin/, MIT): on `post-autoload-dump` its
3//! `DumpCommand` writes `vendor/pest-plugins.json` — `json_encode(...,
4//! JSON_PRETTY_PRINT)` of the `array_merge` of every installed package's
5//! `extra.pest.plugins` list, in the local repository's order
6//! (`getCanonicalPackages()`: aliases excluded), the root package last.
7//! Identical from v1.0.0 to v5.0.0 (the four versions the corpus holds).
8//!
9//! The local repository's order is the caller's business. Composer's own
10//! is not reproducible: on a fresh install `LibraryInstaller::install`
11//! adds a package to the repository when its extraction promise resolves,
12//! so the list follows the completion order of parallel `unzip`s (the
13//! corpus shows the small `pest-plugin-laravel` before `pest` every time,
14//! by size). vivacity writes the operation order — the previous
15//! installed.json entries minus the removed and updated ones, then the
16//! (re)installed ones — which `local_repository_order` builds; the
17//! harnesses compare the file sorted, like `include_paths.php`.
18
19use crate::error::{Error, Result};
20use crate::phpjson::{php_json_encode_with, EncodeOptions};
21use serde_json::Value;
22use std::path::Path;
23
24pub const PLUGIN_NAME: &str = "pestphp/pest-plugin";
25pub const CACHE_FILE: &str = "pest-plugins.json";
26
27/// `extra.pest.plugins` of one package config, as `array_merge` sees it:
28/// a list contributes its values, anything else nothing.
29fn plugins_of(extra: Option<&Value>) -> Vec<Value> {
30    match extra
31        .and_then(|e| e.get("pest"))
32        .and_then(|p| p.get("plugins"))
33    {
34        Some(Value::Array(items)) => items.clone(),
35        Some(Value::Object(map)) => map.values().cloned().collect(),
36        _ => Vec::new(),
37    }
38}
39
40/// `DumpCommand::execute`: `packages` are the installed packages' `extra`
41/// values in local-repository order, `root_extra` the root manifest's.
42pub fn write_pest_plugins(
43    vendor_dir: &Path,
44    packages: &[Option<&Value>],
45    root_extra: Option<&Value>,
46) -> Result<()> {
47    let mut plugins: Vec<Value> = Vec::new();
48    for extra in packages {
49        plugins.extend(plugins_of(*extra));
50    }
51    plugins.extend(plugins_of(root_extra));
52    // `json_encode($plugins, JSON_PRETTY_PRINT)`: slashes and unicode
53    // escaped, four-space indentation, no trailing newline.
54    let text = php_json_encode_with(
55        &Value::Array(plugins),
56        EncodeOptions {
57            pretty: true,
58            escape_slashes: true,
59            escape_unicode: true,
60        },
61    )?;
62    let path = vendor_dir.join(CACHE_FILE);
63    if std::fs::read_to_string(&path).ok().as_deref() == Some(text.as_str()) {
64        return Ok(());
65    }
66    std::fs::write(&path, text).map_err(Error::io(&path))?;
67    Ok(())
68}
69
70/// `Manager::uninstall`: the cache file goes with the plugin.
71pub fn remove_pest_plugins(vendor_dir: &Path) -> Result<()> {
72    let path = vendor_dir.join(CACHE_FILE);
73    match std::fs::remove_file(&path) {
74        Ok(()) => Ok(()),
75        Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(()),
76        Err(e) => Err(Error::io(&path)(e)),
77    }
78}
79
80/// The order of `InstalledRepository::getCanonicalPackages()` after a
81/// transaction: the packages already installed keep installed.json's
82/// order (minus the removed and the updated ones), the installed and
83/// updated ones follow in operation order.
84pub fn local_repository_order<'a>(
85    previous: &[&'a str],
86    removed_or_updated: &[&str],
87    installed: &[&'a str],
88) -> Vec<&'a str> {
89    let mut out: Vec<&str> = previous
90        .iter()
91        .copied()
92        .filter(|n| !removed_or_updated.contains(n) && !installed.contains(n))
93        .collect();
94    out.extend(installed.iter().copied());
95    out
96}
97
98#[cfg(test)]
99mod tests {
100    use super::*;
101    use serde_json::json;
102
103    #[test]
104    fn dump_like_the_plugin() {
105        let tmp = tempfile::tempdir().expect("tmp");
106        let a = json!({"pest": {"plugins": ["Pest\\Laravel\\Plugin"]}});
107        let b = json!({"other": true});
108        let c = json!({"pest": {"plugins": ["Pest\\Plugins\\Foo", "Pest\\Plugins\\Bar"]}});
109        let root = json!({"pest": {"plugins": ["App\\PestPlugin"]}});
110        write_pest_plugins(
111            tmp.path(),
112            &[Some(&a), Some(&b), None, Some(&c)],
113            Some(&root),
114        )
115        .unwrap();
116        let text = std::fs::read_to_string(tmp.path().join("pest-plugins.json")).unwrap();
117        assert_eq!(
118            text,
119            "[\n    \"Pest\\\\Laravel\\\\Plugin\",\n    \"Pest\\\\Plugins\\\\Foo\",\n    \"Pest\\\\Plugins\\\\Bar\",\n    \"App\\\\PestPlugin\"\n]"
120        );
121        write_pest_plugins(tmp.path(), &[], None).unwrap();
122        assert_eq!(
123            std::fs::read_to_string(tmp.path().join("pest-plugins.json")).unwrap(),
124            "[]"
125        );
126    }
127
128    #[test]
129    fn order_after_a_transaction() {
130        let order = local_repository_order(&["a/a", "b/b", "c/c"], &["b/b"], &["d/d", "b/b"]);
131        assert_eq!(order, vec!["a/a", "c/c", "d/d", "b/b"]);
132    }
133}