Skip to content

Commit bb940f9

Browse files
Auto-updater for the Tiled extension.
1 parent e180663 commit bb940f9

File tree

6 files changed

+167
-1
lines changed

6 files changed

+167
-1
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,5 @@
22
*.png
33
*.tiled-session
44
*.tmx
5+
bundle.json
56
node_modules

extensions/include/session.mjs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
const ACCESS_TOKEN_PROPERTY = 'wikiAccessToken';
22
const LAST_LANGUAGE_PROPERTY = 'wikiLastLanguage';
3+
const DISABLE_UPDATE_CHECKS = 'wikiDisableUpdateChecks';
34

45
/**
56
* Retrieves the path to the Tiled session file.
@@ -75,3 +76,18 @@ export function storeLastLanguage(language) {
7576
setSessionProperty(LAST_LANGUAGE_PROPERTY, language);
7677
}
7778

79+
/**
80+
* Checks if automatic update checks are disabled.
81+
* @returns {boolean} True if automatic update checks are disabled
82+
*/
83+
export function areAutoUpdateChecksDisabled() {
84+
return getSessionProperty(DISABLE_UPDATE_CHECKS) === true;
85+
}
86+
87+
/**
88+
* Sets whether automatic update checks are disabled.
89+
* @param {boolean} disabled True to disable automatic update checks
90+
*/
91+
export function storeAutoUpdateChecksDisabled(disabled) {
92+
setSessionProperty(DISABLE_UPDATE_CHECKS, disabled);
93+
}

extensions/update.mjs

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
import { httpGet } from './include/api.mjs';
2+
import {
3+
areAutoUpdateChecksDisabled,
4+
storeAutoUpdateChecksDisabled,
5+
} from './include/session.mjs';
6+
7+
// Conflict with the DOM File type.
8+
const TiledFile = /** @type {any} */ (File);
9+
10+
/**
11+
* Checks if the repository has been set up using Git.
12+
* @returns {boolean} If the repository is using Git
13+
*/
14+
function isGit() {
15+
const p = new Process();
16+
p.workingDirectory = tiled.project.extensionsPath;
17+
const status = p.exec('git', ['status'], false);
18+
return status === 0;
19+
}
20+
21+
/**
22+
* Runs `git pull`.
23+
*/
24+
function pullGit() {
25+
const p = new Process();
26+
p.workingDirectory = tiled.project.extensionsPath;
27+
const status = p.exec('git', ['pull'], false);
28+
if (status !== 0) {
29+
tiled.alert(`Failed to pull updates from Git: ${p.readStdErr()}`);
30+
} else {
31+
tiled.log(p.readStdOut());
32+
}
33+
}
34+
35+
/**
36+
* Recursively retrieves all files in a directory and its subdirectories that
37+
* are relevant to the update process.
38+
* @param {string} dir Directory to inspect for files
39+
* @param {string[]} prefix Base directory for relative paths
40+
* @returns {Record<string, string>}
41+
*/
42+
function getAllUpdaterFiles(dir, prefix = []) {
43+
const /** @type {Record<string, string>} */ files = {};
44+
for (const fileName of TiledFile.directoryEntries(dir, 2, 0)) {
45+
if (!fileName.endsWith('.mjs') && !fileName.endsWith('.svg')) {
46+
continue;
47+
}
48+
const file = new TextFile(`${dir}/${fileName}`, TextFile.ReadOnly);
49+
files[[...prefix, fileName].join('/')] = file.readAll();
50+
file.close();
51+
}
52+
for (const directory of TiledFile.directoryEntries(dir, 24577, 0)) {
53+
Object.assign(files, getAllUpdaterFiles(
54+
FileInfo.joinPaths(dir, directory),
55+
[...prefix, directory]
56+
));
57+
}
58+
return files;
59+
}
60+
61+
/**
62+
* Prompts the user to update the extension if any changes have been detected
63+
* and auto-update is enabled.
64+
*/
65+
function update() {
66+
if (areAutoUpdateChecksDisabled()) {
67+
return;
68+
}
69+
const localFiles = getAllUpdaterFiles(tiled.project.extensionsPath);
70+
tiled.log(JSON.stringify(Object.keys(localFiles)));
71+
72+
httpGet('https://maps.undertale.wiki/bundle.json').then(bundle => {
73+
const onlyLocalFiles = Object.keys(localFiles).filter(path => !(path in bundle));
74+
const diffFiles = Object.entries(bundle).filter(([path, content]) => {
75+
const filePath = FileInfo.joinPaths(tiled.project.extensionsPath, path);
76+
if (!TiledFile.exists(filePath)) {
77+
return true;
78+
}
79+
const file = new TextFile(filePath, TextFile.ReadOnly);
80+
const localContent = file.readAll();
81+
file.close();
82+
return localContent !== content;
83+
});
84+
if (onlyLocalFiles.length === 0 && diffFiles.length === 0) {
85+
tiled.log('Extension is up to date.');
86+
return;
87+
}
88+
if (isGit()) {
89+
if (tiled.confirm('Updates are available for the wiki extension, but you are using Git. Do you want to run git pull? (You can disable these prompts using Edit > Disable tiled-datamaps update checks.)')) {
90+
pullGit();
91+
}
92+
return;
93+
}
94+
if (!tiled.confirm(`Updates are available for the wiki extension! Do you want to update now? (You can disable these prompts using Edit > Disable tiled-datamaps update checks.)`)) {
95+
return;
96+
}
97+
for (const path of onlyLocalFiles) {
98+
TiledFile.remove(FileInfo.joinPaths(tiled.project.extensionsPath, path));
99+
}
100+
for (const [path, content] of diffFiles) {
101+
const filePath = FileInfo.joinPaths(tiled.project.extensionsPath, path);
102+
TiledFile.makePath(FileInfo.path(filePath));
103+
const file = new TextFile(filePath, TextFile.WriteOnly);
104+
file.write(content);
105+
file.commit();
106+
}
107+
tiled.alert('Extension updated successfully! Please restart Tiled to apply the updates.');
108+
}).catch(error => {
109+
tiled.alert(`Failed to check for updates: ${error.message}. Check the console for more details.`);
110+
tiled.log(`Error details: ${error.stack}`);
111+
});
112+
}
113+
114+
const enablePopup = tiled.registerAction('WikiDisableUpdateChecks', () => {
115+
storeAutoUpdateChecksDisabled(enablePopup.checked);
116+
});
117+
enablePopup.checkable = true;
118+
enablePopup.checked = areAutoUpdateChecksDisabled();
119+
enablePopup.icon = 'images/wiki.svg';
120+
enablePopup.iconVisibleInMenu = false;
121+
enablePopup.text = 'Disable tiled-datamaps update checks';
122+
123+
tiled.extendMenu('Edit', [
124+
{
125+
action: 'WikiDisableUpdateChecks'
126+
}
127+
]);
128+
129+
update();

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,5 +27,6 @@
2727
"typescript": "5.9.3"
2828
},
2929
"packageManager": "npm@11.6.2",
30-
"private": true
30+
"private": true,
31+
"type": "module"
3132
}

pages/bundle.js

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import {readdir, readFile, writeFile} from 'fs/promises';
2+
import {join} from 'path';
3+
import {fileURLToPath} from 'url';
4+
5+
const bundle = {};
6+
const scriptDir = join(fileURLToPath(import.meta.url), '..');
7+
const extensionsDir = join(scriptDir, '..', 'extensions');
8+
for (const dir of await readdir(extensionsDir, {
9+
recursive: true,
10+
withFileTypes: true,
11+
})) {
12+
if (dir.isFile() && (dir.name.endsWith('.mjs') || dir.name.endsWith('.svg'))) {
13+
const filePath = join(dir.parentPath, dir.name);
14+
const content = await readFile(filePath, 'utf-8');
15+
const relativePath = filePath.slice(extensionsDir.length + 1);
16+
bundle[relativePath] = content;
17+
}
18+
}
19+
await writeFile(join(scriptDir, 'bundle.json'), JSON.stringify(bundle));
File renamed without changes.

0 commit comments

Comments
 (0)