Skip to main content

dev_prune/adapters/
cocos.rs

1// Copyright 2026 VKrishna04
2// SPDX-License-Identifier: Apache-2.0
3
4// Cocos Creator imported-asset cache adapter.
5//
6// A Cocos Creator project is anchored by a `package.json` at its root, the same file
7// name every Node project has, which is why detection reads it: Creator 3.x writes a
8// `"creator"` key into it, and Creator 2.x names `"cocos-creator-js"` as its engine.
9// A `package.json` with neither is an ordinary Node project and none of this
10// adapter's business; the npm adapter already handles it.
11//
12// Beside the manifest the editor writes `library/` (imported assets in engine format)
13// and `temp/` (its scratch space). Both regenerate from the committed sources the
14// next time the editor opens the project, and both are in Creator's own generated
15// `.gitignore`. `build/` is not claimed: it is the published output a person exported
16// on purpose. Unlike Unity there is no known on-disk marker for "the editor has this
17// project open", so the idle window is the only guard against a live editor; the
18// opt-in and the longer build window are what keep that risk small.
19//
20// Opt-in, and held to `build_idle_days`: getting `library/` back means the editor
21// re-importing every asset on the next open.
22
23use super::{BloatDir, EnforcePolicy, PackageManager, dir_size};
24use anyhow::{Result, anyhow};
25use std::fs;
26use std::path::Path;
27
28/// The manifest at the project root, shared with every Node project, which is why
29/// detection reads its contents instead of trusting the name.
30const MANIFEST: &str = "package.json";
31
32/// The editor's cache directories. Never `build/`, which is exported output.
33const CACHE_DIRS: &[&str] = &["library", "temp"];
34
35/// True when this `package.json` belongs to Cocos Creator rather than plain Node:
36/// Creator 3.x writes a `"creator"` key, Creator 2.x a `"cocos-creator-js"` engine.
37fn is_creator_manifest(content: &str) -> bool {
38    content.contains("\"creator\"") || content.contains("cocos-creator")
39}
40
41/// Cocos Creator imported-asset cache adapter. Opt-in; see the module comment.
42pub struct Cocos;
43
44impl PackageManager for Cocos {
45    fn name(&self) -> &'static str {
46        "cocos"
47    }
48
49    fn detect(&self, path: &Path) -> bool {
50        let manifest = path.join(MANIFEST);
51        manifest.is_file()
52            && fs::read_to_string(&manifest).is_ok_and(|content| is_creator_manifest(&content))
53    }
54
55    fn bloat_dirs(&self, path: &Path) -> Vec<BloatDir> {
56        CACHE_DIRS
57            .iter()
58            .map(|name| path.join(name))
59            .filter(|dir| dir.is_dir())
60            .map(|dir| BloatDir {
61                name: dir
62                    .file_name()
63                    .unwrap_or_default()
64                    .to_string_lossy()
65                    .into_owned(),
66                size_bytes: dir_size(&dir),
67                path: dir,
68                shared_bytes: 0,
69            })
70            .collect()
71    }
72
73    /// What is checked is that `package.json` is still readable and still names Cocos
74    /// Creator, because it is what marks the caches as the editor's to regenerate.
75    /// Running the editor to find out would start the re-import in the middle of a
76    /// delete pass.
77    fn enforce_lockfile(&self, path: &Path, _policy: EnforcePolicy) -> Result<()> {
78        let manifest = path.join(MANIFEST);
79        let content = fs::read_to_string(&manifest).map_err(|e| {
80            anyhow!("`{MANIFEST}` could not be read ({e}): nothing to re-import the assets from.")
81        })?;
82        if !is_creator_manifest(&content) {
83            return Err(anyhow!(
84                "`{MANIFEST}` no longer names Cocos Creator: refusing to treat the \
85                 imported-asset cache as regenerable from it."
86            ));
87        }
88        Ok(())
89    }
90
91    fn restore(&self, _path: &Path, _timeout: std::time::Duration) -> Result<()> {
92        println!(
93            "Cocos Creator's imported-asset cache regenerates when the editor next \
94             opens the project"
95        );
96        Ok(())
97    }
98
99    fn lockfiles(&self) -> &'static [&'static str] {
100        &[MANIFEST]
101    }
102
103    fn opt_in(&self) -> bool {
104        true
105    }
106}
107
108#[cfg(test)]
109mod tests {
110    use super::*;
111    use std::path::PathBuf;
112    use tempfile::tempdir;
113
114    /// A project root holding a plausible Creator 3.x `package.json`.
115    fn project(dir: &Path) -> PathBuf {
116        fs::write(
117            dir.join(MANIFEST),
118            "{\n  \"name\": \"demo\",\n  \"creator\": {\n    \"version\": \"3.8.0\"\n  }\n}\n",
119        )
120        .unwrap();
121        dir.to_path_buf()
122    }
123
124    fn claimed(project: &Path) -> Vec<String> {
125        Cocos
126            .bloat_dirs(project)
127            .into_iter()
128            .map(|b| b.name)
129            .collect()
130    }
131
132    #[test]
133    fn detects_on_a_creator_manifest_only() {
134        let dir = tempdir().unwrap();
135        assert!(!Cocos.detect(dir.path()));
136        // A plain Node project's package.json is the npm adapter's, not this one's.
137        fs::write(
138            dir.path().join(MANIFEST),
139            "{\n  \"name\": \"demo\",\n  \"dependencies\": {}\n}\n",
140        )
141        .unwrap();
142        assert!(!Cocos.detect(dir.path()));
143        project(dir.path());
144        assert!(Cocos.detect(dir.path()));
145    }
146
147    #[test]
148    fn a_creator_2x_engine_line_also_detects() {
149        let dir = tempdir().unwrap();
150        fs::write(
151            dir.path().join(MANIFEST),
152            "{\n  \"name\": \"demo\",\n  \"engine\": \"cocos-creator-js\",\n  \"version\": \"2.4.13\"\n}\n",
153        )
154        .unwrap();
155        assert!(Cocos.detect(dir.path()));
156    }
157
158    #[test]
159    fn the_caches_are_claimed_and_the_exported_build_is_not() {
160        // `build/` is what someone exported on purpose; the claim stops at the
161        // editor's own caches.
162        let dir = tempdir().unwrap();
163        let root = project(dir.path());
164        fs::create_dir(root.join("library")).unwrap();
165        fs::write(root.join("library").join("imports.json"), "cache").unwrap();
166        fs::create_dir(root.join("temp")).unwrap();
167        fs::create_dir(root.join("build")).unwrap();
168        fs::create_dir(root.join("assets")).unwrap();
169
170        assert_eq!(claimed(&root), vec!["library", "temp"]);
171    }
172
173    #[test]
174    fn a_missing_or_bogus_manifest_is_refused() {
175        let dir = tempdir().unwrap();
176        let policy = EnforcePolicy::default();
177        assert!(Cocos.enforce_lockfile(dir.path(), policy).is_err());
178        fs::write(dir.path().join(MANIFEST), "{\"name\": \"demo\"}").unwrap();
179        assert!(Cocos.enforce_lockfile(dir.path(), policy).is_err());
180        project(dir.path());
181        assert!(Cocos.enforce_lockfile(dir.path(), policy).is_ok());
182    }
183
184    #[test]
185    fn cocos_is_opt_in() {
186        assert!(Cocos.opt_in());
187    }
188}