@volter/sdk 0.5.203 → 0.5.204
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@volter/sdk",
|
|
3
3
|
"author": "Volter AI, Inc.",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
|
-
"version": "0.5.
|
|
5
|
+
"version": "0.5.204",
|
|
6
6
|
"publishConfig": {
|
|
7
7
|
"access": "public"
|
|
8
8
|
},
|
|
@@ -487,7 +487,7 @@
|
|
|
487
487
|
"@fortawesome/react-fontawesome": "^3.2.0",
|
|
488
488
|
"@storybook/react": "10.6.0",
|
|
489
489
|
"@types/node": "^22.0.0",
|
|
490
|
-
"@volter/project": "0.5.
|
|
490
|
+
"@volter/project": "0.5.204",
|
|
491
491
|
"mediabunny": "1.56.2",
|
|
492
492
|
"postcss": "8.5.28",
|
|
493
493
|
"rrweb": "2.1.6",
|
|
@@ -268,8 +268,10 @@ function resolveAreaScope(): Scope {
|
|
|
268
268
|
if (!first) {
|
|
269
269
|
throw new Error("No workspace area is showing, so scope 'area' has nothing to reach.");
|
|
270
270
|
}
|
|
271
|
-
|
|
272
|
-
|
|
271
|
+
// Every area the scope covers is named, so a reader of the answer never takes one area's id
|
|
272
|
+
// for where a match was when several are showing.
|
|
273
|
+
const ids = [...new Set(shown.map((element) => element.dataset['workspaceDocumentId'] ?? ''))];
|
|
274
|
+
return { container: first, extraRoots: rest, name: 'area', id: ids.join(', '), title: ids.length > 1 ? `${ids.length} workspace areas` : 'workspace area' };
|
|
273
275
|
}
|
|
274
276
|
|
|
275
277
|
/** The application menu bar, by the stamp `ApplicationMenus` writes on its own root. The menus
|
|
@@ -50,6 +50,17 @@ export type StartupRecovery =
|
|
|
50
50
|
/** As on `use-compatible-editor`. */
|
|
51
51
|
summary?: string;
|
|
52
52
|
}
|
|
53
|
+
| {
|
|
54
|
+
/**
|
|
55
|
+
* A project this product can open once it is brought up to date by the newest release's own `upgrade`
|
|
56
|
+
* or by hand (a project made before 0.5.203 keeps its adapter at its root); `guidance` says how.
|
|
57
|
+
*/
|
|
58
|
+
kind: 'bring-project-up-to-date';
|
|
59
|
+
title: 'Bring this project up to date';
|
|
60
|
+
guidance: string;
|
|
61
|
+
/** As on `use-compatible-editor`. */
|
|
62
|
+
summary?: string;
|
|
63
|
+
}
|
|
53
64
|
| {
|
|
54
65
|
/** The folder is not a project this product can open; `guidance` says how to make one. */
|
|
55
66
|
kind: 'make-project';
|
|
@@ -92,6 +103,7 @@ export function isStartupRecovery(value: unknown): value is StartupRecovery {
|
|
|
92
103
|
case 'restart-editor':
|
|
93
104
|
return verbs === 'edit .' || verbs === 'close | edit .';
|
|
94
105
|
case 'use-compatible-editor':
|
|
106
|
+
case 'bring-project-up-to-date':
|
|
95
107
|
case 'make-project':
|
|
96
108
|
return candidate['verbs'] === undefined;
|
|
97
109
|
case 'upgrade-project':
|
|
@@ -32,13 +32,13 @@
|
|
|
32
32
|
*/
|
|
33
33
|
import { spawn } from 'node:child_process';
|
|
34
34
|
import { existsSync, lstatSync, readFileSync } from 'node:fs';
|
|
35
|
-
import { readFile, rename, rm, writeFile } from 'node:fs/promises';
|
|
36
|
-
import { dirname, join, resolve } from 'node:path';
|
|
35
|
+
import { mkdir, readdir, readFile, rename, rm, rmdir, writeFile } from 'node:fs/promises';
|
|
36
|
+
import { dirname, join, relative, resolve, sep } from 'node:path';
|
|
37
37
|
import { MANIFEST_FILENAME } from '@volter/project/manifest/filename';
|
|
38
38
|
import { hasManifest } from '@volter/project/manifest/locate';
|
|
39
39
|
import { compareSemver } from './editor-compatibility';
|
|
40
40
|
|
|
41
|
-
export const UPGRADE_USAGE = "upgrade [version] # move this project's @volter packages and its engine pin to one release (default: latest)";
|
|
41
|
+
export const UPGRADE_USAGE = "upgrade [version] # move this project's @volter packages and its engine pin to one release (default: latest); from a project made before 0.5.203, run the newest release's own: npx <package>@latest upgrade";
|
|
42
42
|
|
|
43
43
|
/** A product name that is no longer published, as a project still spells it. */
|
|
44
44
|
export interface RetiredProduct {
|
|
@@ -57,12 +57,15 @@ export interface UpgradingProduct {
|
|
|
57
57
|
readonly dir: string;
|
|
58
58
|
/** The names this product replaced: a project declaring one is moved onto this product. */
|
|
59
59
|
readonly replaces?: readonly RetiredProduct[];
|
|
60
|
+
/** This project's `node_modules` is linked by the product after the upgrade (the game editor's runtime image),
|
|
61
|
+
* even where no link stands yet: it is never told to `npm install`, and no lockfile is rewritten. */
|
|
62
|
+
readonly linksNodeModules?: boolean;
|
|
60
63
|
}
|
|
61
64
|
|
|
62
65
|
/** The ONE line that moves a project onto `product`, from any version — including a project on a
|
|
63
66
|
* name `product` replaced, whose own installed command does not know `product` exists. */
|
|
64
67
|
export function upgradeLine(product: Pick<UpgradingProduct, 'packageName'>): string {
|
|
65
|
-
return `npx ${product.packageName} upgrade`;
|
|
68
|
+
return `npx ${product.packageName}@latest upgrade`;
|
|
66
69
|
}
|
|
67
70
|
|
|
68
71
|
/** The retired product a project's `package.json` still declares, or null. */
|
|
@@ -88,6 +91,125 @@ export function retiredProjectError(product: Pick<UpgradingProduct, 'packageName
|
|
|
88
91
|
/** The files `create` writes that name the product's package or command, project-relative. */
|
|
89
92
|
const RENAMED_PRODUCT_FILES = ['.mcp.json', '.codex/config.toml', 'AGENTS.md', 'CLAUDE.md'] as const;
|
|
90
93
|
|
|
94
|
+
/** The kit packages 0.5.203 renamed (#284), old name first. */
|
|
95
|
+
const RENAMED_KIT_PACKAGES = [
|
|
96
|
+
['@volter/editor-project', '@volter/project'],
|
|
97
|
+
['@volter/editor-sdk', '@volter/sdk'],
|
|
98
|
+
['@volter/editor-live', '@volter/live'],
|
|
99
|
+
['@volter/editor-model-play', '@volter/play'],
|
|
100
|
+
] as const;
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* What 0.5.203 (#286) moved from where a project kept it to `editor/`: the adapter from the project's root, and the
|
|
104
|
+
* project's contributions and tools from `src/` (`tool-contribution-convention` read `src/contributions` and
|
|
105
|
+
* `src/tools` until then). Project-relative, from and to.
|
|
106
|
+
*/
|
|
107
|
+
const MOVED_TO_EDITOR = [
|
|
108
|
+
['volter.adapter.ts', 'editor/volter.adapter.ts'],
|
|
109
|
+
['src/contributions', 'editor/contributions'],
|
|
110
|
+
['src/tools', 'editor/tools'],
|
|
111
|
+
] as const;
|
|
112
|
+
|
|
113
|
+
/** Every file under `path` (or `path` itself, a file); none when it is gone. */
|
|
114
|
+
async function allFiles(path: string): Promise<string[]> {
|
|
115
|
+
let entries: import('node:fs').Dirent[];
|
|
116
|
+
try { entries = await readdir(path, { withFileTypes: true }); } catch { return existsSync(path) ? [path] : []; }
|
|
117
|
+
const files: string[] = [];
|
|
118
|
+
for (const entry of entries) {
|
|
119
|
+
const child = join(path, entry.name);
|
|
120
|
+
if (entry.isDirectory()) files.push(...await allFiles(child));
|
|
121
|
+
else files.push(child);
|
|
122
|
+
}
|
|
123
|
+
return files;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** Every script file under `dir`, skipping `node_modules` and dot-folders. */
|
|
127
|
+
async function scriptFiles(dir: string): Promise<string[]> {
|
|
128
|
+
const files: string[] = [];
|
|
129
|
+
let entries: import('node:fs').Dirent[];
|
|
130
|
+
try { entries = await readdir(dir, { withFileTypes: true }); } catch { return files; }
|
|
131
|
+
for (const entry of entries) {
|
|
132
|
+
if (entry.name === 'node_modules' || entry.name.startsWith('.')) continue;
|
|
133
|
+
const path = join(dir, entry.name);
|
|
134
|
+
if (entry.isDirectory()) files.push(...await scriptFiles(path));
|
|
135
|
+
else if (/\.(?:[cm]?[jt]sx?)$/.test(entry.name)) files.push(path);
|
|
136
|
+
}
|
|
137
|
+
return files;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** A project's own source that may import the kit: its root script files (the adapter, `vite.config.ts`),
|
|
141
|
+
* and everything under `editor/`, `src/` and `scripts/`. */
|
|
142
|
+
async function projectSourceFiles(project: string): Promise<string[]> {
|
|
143
|
+
const files: string[] = [];
|
|
144
|
+
try {
|
|
145
|
+
for (const entry of await readdir(project, { withFileTypes: true })) {
|
|
146
|
+
if (entry.isFile() && /\.(?:[cm]?[jt]sx?)$/.test(entry.name)) files.push(join(project, entry.name));
|
|
147
|
+
}
|
|
148
|
+
} catch { /* an unreadable project root is refused before this */ }
|
|
149
|
+
for (const folder of ['editor', 'src', 'scripts']) files.push(...await scriptFiles(join(project, folder)));
|
|
150
|
+
return files;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* `text` naming what 0.5.203 moved where it now is: `src/contributions` and `src/tools` as path segments, and the
|
|
155
|
+
* root adapter (`volter.adapter.ts` not already under `editor/`). For the instructions and the catalog records a
|
|
156
|
+
* project carries, and the path strings in its own scripts (`check-idioms.ts` names both folders).
|
|
157
|
+
*/
|
|
158
|
+
function rewriteMovedPaths(text: string): string {
|
|
159
|
+
return text
|
|
160
|
+
.replace(/(^|[^\w./-])(\.\/)?src\/(contributions|tools)(?=$|[/'"`\s),;:\]]|\.$)/gm, '$1$2editor/$3')
|
|
161
|
+
.replace(/(^|[^\w./-])(\.\/)?volter\.adapter\.ts\b/gm, '$1$2editor/volter.adapter.ts');
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** The text files that name a project's places in words or as data: the instructions `create` wrote (any root
|
|
165
|
+
* Markdown, and the agent skills under `.claude/`, `.agents/` and `.github/`), the root TypeScript configs (their
|
|
166
|
+
* `include`, `exclude` and `paths`) and the catalog's records (`.volter/`, its JSON). Never `node_modules`. */
|
|
167
|
+
async function projectTextFiles(project: string): Promise<string[]> {
|
|
168
|
+
const files: string[] = [];
|
|
169
|
+
try {
|
|
170
|
+
for (const entry of await readdir(project, { withFileTypes: true })) {
|
|
171
|
+
if (entry.isFile() && /(?:\.md|^tsconfig(?:\.[\w-]+)?\.json)$/i.test(entry.name)) files.push(join(project, entry.name));
|
|
172
|
+
}
|
|
173
|
+
} catch { /* an unreadable project root is refused before this */ }
|
|
174
|
+
const under = async (dir: string, pattern: RegExp): Promise<void> => {
|
|
175
|
+
for (const path of await allFiles(dir)) if (!path.includes(`${sep}node_modules${sep}`) && pattern.test(path)) files.push(path);
|
|
176
|
+
};
|
|
177
|
+
for (const folder of ['.claude', '.agents', '.github']) await under(join(project, folder), /\.md$/i);
|
|
178
|
+
await under(join(project, '.volter', 'catalog'), /\.json$/i);
|
|
179
|
+
if (existsSync(join(project, '.volter', 'scaffold-baseline.json'))) files.push(join(project, '.volter', 'scaffold-baseline.json'));
|
|
180
|
+
return files;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** `text` with every import of a renamed kit package (the package itself or a subpath) under its current name, and
|
|
184
|
+
* every path that names its installed folder (`node_modules/@volter/editor-project/src/*`, a tsconfig's `paths`). */
|
|
185
|
+
function renameKitSpecifiers(text: string): string {
|
|
186
|
+
let out = text;
|
|
187
|
+
for (const [from, to] of RENAMED_KIT_PACKAGES) {
|
|
188
|
+
out = out.replace(new RegExp(`(['"\`]|node_modules\\/)${from.replace('/', '\\/')}(?=[/'"\`])`, 'g'), `$1${to}`);
|
|
189
|
+
}
|
|
190
|
+
return out;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* `text`, a module moving from `fromDir` to `toDir` while the files in `moved` (old path to new) move too, with each
|
|
195
|
+
* relative specifier still naming the same file: `from`, `import`, `import()`, `require()`, `new URL()` and
|
|
196
|
+
* `import.meta.glob()`, and a bare `.` or `..`. A specifier whose module and target both stay is left as written.
|
|
197
|
+
*/
|
|
198
|
+
function rebaseRelativeSpecifiers(text: string, fromDir: string, toDir: string, moved: (path: string) => string): string {
|
|
199
|
+
return text.replace(
|
|
200
|
+
/(\bfrom\s*|\bimport\s*\(\s*|\bimport\s+|\brequire\s*\(\s*|\bnew\s+URL\s*\(\s*|\bimport\.meta\.glob\s*\(\s*)(['"`])(\.{1,2}(?:\/[^'"`]*)?)\2/g,
|
|
201
|
+
(whole: string, lead: string, quote: string, spec: string) => {
|
|
202
|
+
const target = resolve(fromDir, spec);
|
|
203
|
+
if (fromDir === toDir && moved(target) === target) return whole;
|
|
204
|
+
let next = relative(toDir, moved(target)).split('\\').join('/');
|
|
205
|
+
if (next === '') next = '.';
|
|
206
|
+
if (!next.startsWith('.')) next = `./${next}`;
|
|
207
|
+
if (spec.endsWith('/') && !next.endsWith('/')) next = `${next}/`;
|
|
208
|
+
return `${lead}${quote}${next}${quote}`;
|
|
209
|
+
},
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
|
|
91
213
|
/** `text` as a whole word — not part of a longer command, package or name it is the start or the
|
|
92
214
|
* end of (`<command>-x`, a name's plural). A path after it (`<package>/package.json`) or
|
|
93
215
|
* punctuation (a name's `'s`) still ends the word. */
|
|
@@ -121,6 +243,12 @@ function declaredVersion(spec: string | undefined): string | null {
|
|
|
121
243
|
return bare !== undefined && EXACT_VERSION.test(bare) ? bare : null;
|
|
122
244
|
}
|
|
123
245
|
|
|
246
|
+
/** The version a kit package is declared at, for telling which packages move together: an exact spec's, or a
|
|
247
|
+
* local tarball's (`file:…/volter-play-0.5.202.tgz`, how a project built from a checkout declares the kit). */
|
|
248
|
+
function kitVersion(spec: string | undefined): string | null {
|
|
249
|
+
return declaredVersion(spec) ?? spec?.match(/^file:.*-(\d+\.\d+\.\d+)\.tgz$/)?.[1] ?? null;
|
|
250
|
+
}
|
|
251
|
+
|
|
124
252
|
/**
|
|
125
253
|
* Write every planned file, or none: each to a temp file beside it first, then all renamed into
|
|
126
254
|
* place. A failure while writing leaves the project as it was; a failure between the renames
|
|
@@ -128,7 +256,7 @@ function declaredVersion(spec: string | undefined): string | null {
|
|
|
128
256
|
* one after the other, so a failure on the second left a project with new packages and the old
|
|
129
257
|
* pin — exactly the mismatch this verb exists to end.
|
|
130
258
|
*/
|
|
131
|
-
async function writeAll(files: readonly { readonly path: string; readonly content: string; readonly original: string }[]): Promise<void> {
|
|
259
|
+
async function writeAll(files: readonly { readonly path: string; readonly content: string; readonly original: string; readonly created?: boolean }[]): Promise<void> {
|
|
132
260
|
const temp = (path: string) => `${path}.upgrade-${process.pid}.tmp`;
|
|
133
261
|
try {
|
|
134
262
|
for (const file of files) await writeFile(temp(file.path), file.content);
|
|
@@ -140,7 +268,8 @@ async function writeAll(files: readonly { readonly path: string; readonly conten
|
|
|
140
268
|
try {
|
|
141
269
|
for (const file of files) { await rename(temp(file.path), file.path); moved.push(file); }
|
|
142
270
|
} catch (error) {
|
|
143
|
-
|
|
271
|
+
// A file this upgrade made is removed again; any other gets its old content back.
|
|
272
|
+
for (const file of moved) await (file.created ? rm(file.path, { force: true }) : writeFile(file.path, file.original)).catch(() => {});
|
|
144
273
|
await Promise.all(files.map(file => rm(temp(file.path), { force: true })));
|
|
145
274
|
throw new Error(`The upgrade could not be put in place, and what had moved was put back: ${error instanceof Error ? error.message : String(error)}`);
|
|
146
275
|
}
|
|
@@ -184,10 +313,16 @@ function jsonLayout(raw: string): (value: unknown) => string {
|
|
|
184
313
|
/** `npm view <spec> version dependencies --json`, parsed. */
|
|
185
314
|
function npmView(spec: string): Promise<Release> {
|
|
186
315
|
return new Promise((done, fail) => {
|
|
187
|
-
// npm is npm.cmd on Windows, and node refuses to spawn a .cmd without a shell (EINVAL).
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
316
|
+
// npm is npm.cmd on Windows, and node refuses to spawn a .cmd without a shell (EINVAL). The shell gets one
|
|
317
|
+
// command line (arguments beside `shell` print Node's DEP0190 warning), so the spec, which carries the version
|
|
318
|
+
// the person typed, is a package name and a version or tag, nothing a shell reads.
|
|
319
|
+
if (!/^@?[\w.-]+(?:\/[\w.-]+)?@[\w.+-]+$/.test(spec)) {
|
|
320
|
+
fail(new Error(`${spec} is not a package and a version or tag.`));
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
const child = process.platform === 'win32'
|
|
324
|
+
? spawn(`npm view ${spec} version dependencies --json`, { windowsHide: true, stdio: ['ignore', 'pipe', 'pipe'], shell: true })
|
|
325
|
+
: spawn('npm', ['view', spec, 'version', 'dependencies', '--json'], { stdio: ['ignore', 'pipe', 'pipe'] });
|
|
191
326
|
let out = '';
|
|
192
327
|
let err = '';
|
|
193
328
|
child.stdout.on('data', (chunk: Buffer) => { out += chunk.toString('utf8'); });
|
|
@@ -249,7 +384,8 @@ export async function upgradeProject(product: UpgradingProduct, requested?: stri
|
|
|
249
384
|
// the new kit version. Any other @volter package is the author's choice and is named, not moved.
|
|
250
385
|
const packageRaw = await readFile(packagePath, 'utf8');
|
|
251
386
|
const pkg = JSON.parse(packageRaw) as PackageJson;
|
|
252
|
-
const previousKit = pkg.devDependencies?.['@volter/project'] ?? pkg.dependencies?.['@volter/project']
|
|
387
|
+
const previousKit = pkg.devDependencies?.['@volter/project'] ?? pkg.dependencies?.['@volter/project']
|
|
388
|
+
?? pkg.devDependencies?.['@volter/editor-project'] ?? pkg.dependencies?.['@volter/editor-project'];
|
|
253
389
|
// Read before a retired name is moved below: a project on a retired name declares none of
|
|
254
390
|
// this product, and its direction is then read by its engine pin.
|
|
255
391
|
const currentProduct = declaredVersion(pkg.dependencies?.[product.packageName] ?? pkg.devDependencies?.[product.packageName]);
|
|
@@ -272,7 +408,7 @@ export async function upgradeProject(product: UpgradingProduct, requested?: stri
|
|
|
272
408
|
// 0. A RETIRED NAME becomes this product, where it stood and at the release — a project that
|
|
273
409
|
// already declares this product as well just loses the old row.
|
|
274
410
|
const retired = declaredRetiredProduct(product, project);
|
|
275
|
-
const textFiles: { path: string; content: string; original: string }[] = [];
|
|
411
|
+
const textFiles: { path: string; content: string; original: string; created?: boolean }[] = [];
|
|
276
412
|
if (retired !== null) {
|
|
277
413
|
const declaresProduct = pkg.dependencies?.[product.packageName] !== undefined || pkg.devDependencies?.[product.packageName] !== undefined;
|
|
278
414
|
for (const section of ['dependencies', 'devDependencies'] as const) {
|
|
@@ -316,12 +452,110 @@ export async function upgradeProject(product: UpgradingProduct, requested?: stri
|
|
|
316
452
|
changed.push(`${file}: names ${product.packageName} and \`${product.command}\``);
|
|
317
453
|
}
|
|
318
454
|
}
|
|
455
|
+
// 0b. THE KIT'S OLD NAMES AND THE EDITOR FOLDER (0.5.203: #284 renamed the runtime packages, #286 moved a
|
|
456
|
+
// project's editor side into `editor/`). A project made before 0.5.203 declares the kit under its old names and
|
|
457
|
+
// keeps its editor side where #286 moved it from (the adapter at its root, contributions and tools in `src/`),
|
|
458
|
+
// and the release names neither; left alone, the upgraded project was refused on its next open, or opened
|
|
459
|
+
// without its tools. The names move where they stand, in package.json and in the project's own source, and what
|
|
460
|
+
// #286 moved moves into `editor/`, each file with its relative imports rebased.
|
|
461
|
+
for (const section of ['dependencies', 'devDependencies'] as const) {
|
|
462
|
+
const declared = pkg[section];
|
|
463
|
+
if (declared === undefined) continue;
|
|
464
|
+
for (const [from, to] of RENAMED_KIT_PACKAGES) {
|
|
465
|
+
const current = declared[from];
|
|
466
|
+
if (current === undefined) continue;
|
|
467
|
+
if (declared[to] !== undefined) {
|
|
468
|
+
delete declared[from];
|
|
469
|
+
changed.push(`package.json ${section}: ${from} removed (${to} is already declared)`);
|
|
470
|
+
} else {
|
|
471
|
+
pkg[section] = renameKey(pkg[section]!, from, to, current);
|
|
472
|
+
changed.push(`package.json ${section}: ${from} -> ${to}`);
|
|
473
|
+
}
|
|
474
|
+
packagesChanged = true;
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
const projectPath = (path: string) => relative(project, path).split('\\').join('/');
|
|
478
|
+
// FILE BY FILE, so a second run finishes what a first left: each file of a moved folder (or the adapter) moves
|
|
479
|
+
// unless a file is already at its destination, which is said and left. A destination folder that exists (empty,
|
|
480
|
+
// or holding what a first run moved) is merged into, not taken as a reason to skip the move.
|
|
481
|
+
const moves: { from: string; to: string }[] = [];
|
|
482
|
+
for (const [from, to] of MOVED_TO_EDITOR) {
|
|
483
|
+
const source = join(project, from);
|
|
484
|
+
if (existsSync(source)) moves.push({ from: source, to: join(project, to) });
|
|
485
|
+
}
|
|
486
|
+
const movedPath = (path: string): string => {
|
|
487
|
+
for (const move of moves) {
|
|
488
|
+
if (path === move.from) return move.to;
|
|
489
|
+
if (path.startsWith(move.from + sep)) return move.to + path.slice(move.from.length);
|
|
490
|
+
// The adapter imported without its extension, or as the `.js` a build would emit.
|
|
491
|
+
const bare = move.from.replace(/\.ts$/, '');
|
|
492
|
+
if (bare !== move.from && (path === bare || path === `${bare}.js`)) return move.to.replace(/\.ts$/, '') + path.slice(bare.length);
|
|
493
|
+
}
|
|
494
|
+
return path;
|
|
495
|
+
};
|
|
496
|
+
const collides = (path: string) => movedPath(path) !== path && existsSync(movedPath(path));
|
|
497
|
+
const stranded: string[] = [];
|
|
498
|
+
for (const move of moves) for (const path of await allFiles(move.from)) if (collides(path)) stranded.push(path);
|
|
499
|
+
for (const path of stranded)
|
|
500
|
+
kept.push(`${projectPath(path)} stays where it is and is not read: ${projectPath(movedPath(path))} already exists; keep one of the two by hand`);
|
|
501
|
+
const willMove = (path: string) => movedPath(path) !== path && !stranded.includes(path);
|
|
502
|
+
const leftBehind: string[] = [];
|
|
503
|
+
for (const path of await projectSourceFiles(project)) {
|
|
504
|
+
const original = await readFile(path, 'utf8');
|
|
505
|
+
const destination = willMove(path) ? movedPath(path) : path;
|
|
506
|
+
const content = rewriteMovedPaths(
|
|
507
|
+
rebaseRelativeSpecifiers(renameKitSpecifiers(original), dirname(path), dirname(destination), (p) => (willMove(p) ? movedPath(p) : p)),
|
|
508
|
+
);
|
|
509
|
+
if (destination !== path) {
|
|
510
|
+
textFiles.push({ path: destination, content, original: '', created: true });
|
|
511
|
+
leftBehind.push(path);
|
|
512
|
+
} else if (content !== original) {
|
|
513
|
+
textFiles.push({ path, content, original });
|
|
514
|
+
changed.push(`${projectPath(path)}: names the kit's current packages and the moved files where they now are`);
|
|
515
|
+
// 0.5.203 fails a game build that reaches into editor/ (src/ is the game): a game file that imported one of
|
|
516
|
+
// the moved files now does, and is named so the person moves what it needs out of editor/. A file left behind
|
|
517
|
+
// because its destination exists is already named, and is not read.
|
|
518
|
+
if (path.startsWith(join(project, 'src') + sep) && !stranded.includes(path) && /(['"`])(?:\.{1,2}\/)+[^'"`]*\beditor\//.test(content) && !/(['"`])(?:\.{1,2}\/)+[^'"`]*\beditor\//.test(original))
|
|
519
|
+
kept.push(`${projectPath(path)} now imports from editor/, which a game build refuses from 0.5.203: move what it uses out of editor/ (src/ is the game)`);
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
for (const move of moves) {
|
|
523
|
+
if ((await allFiles(move.from)).some((path) => !stranded.includes(path)))
|
|
524
|
+
changed.push(`${projectPath(move.from)} -> ${projectPath(move.to)} (a project's editor side lives in editor/ from 0.5.203)`);
|
|
525
|
+
}
|
|
526
|
+
// THE FILES THAT NAME THE OLD PLACES IN WORDS OR AS DATA: the instructions `create` wrote (AGENTS.md, IDIOMS.md,
|
|
527
|
+
// CLAUDE.md, the agent skills), the TypeScript configs (the tools' program includes `src/tools`; left, it found no
|
|
528
|
+
// files and `typecheck` failed) and the game editor's catalog records (`.volter/`), which name the files it wrote.
|
|
529
|
+
for (const path of await projectTextFiles(project)) {
|
|
530
|
+
const original = await readFile(path, 'utf8');
|
|
531
|
+
const content = rewriteMovedPaths(renameKitSpecifiers(original));
|
|
532
|
+
if (content === original) continue;
|
|
533
|
+
textFiles.push({ path, content, original });
|
|
534
|
+
changed.push(`${projectPath(path)}: names the moved files and the kit's packages where they now are`);
|
|
535
|
+
}
|
|
536
|
+
// A tool is registered in package.json by its path (`volter.tools`); a registration into a moved folder follows it.
|
|
537
|
+
const volter = pkg['volter'];
|
|
538
|
+
const tools = volter !== null && typeof volter === 'object' && !Array.isArray(volter) ? (volter as { tools?: unknown }).tools : undefined;
|
|
539
|
+
if (Array.isArray(tools)) {
|
|
540
|
+
const rewritten = tools.map((registration: unknown) => {
|
|
541
|
+
const entry = typeof registration === 'string' ? registration : (registration as { entry?: unknown } | null)?.entry;
|
|
542
|
+
if (typeof entry !== 'string' || !entry.startsWith('./')) return registration;
|
|
543
|
+
const next = `./${projectPath(movedPath(join(project, entry.slice(2))))}`;
|
|
544
|
+
if (next === entry) return registration;
|
|
545
|
+
changed.push(`package.json volter.tools: ${entry} -> ${next}`);
|
|
546
|
+
packagesChanged = true;
|
|
547
|
+
return typeof registration === 'string' ? next : { ...(registration as object), entry: next };
|
|
548
|
+
});
|
|
549
|
+
(volter as { tools: unknown[] }).tools = rewritten;
|
|
550
|
+
}
|
|
551
|
+
|
|
319
552
|
for (const section of ['dependencies', 'devDependencies'] as const) {
|
|
320
553
|
const declared = pkg[section];
|
|
321
554
|
if (!declared) continue;
|
|
322
555
|
for (const [name, current] of Object.entries(declared)) {
|
|
323
556
|
if (!name.startsWith('@volter/')) continue;
|
|
324
|
-
const target = name === product.packageName ? release.version : release.dependencies[name]
|
|
557
|
+
const target = name === product.packageName ? release.version : release.dependencies[name]
|
|
558
|
+
?? (current === previousKit || (kitVersion(current) !== null && kitVersion(current) === kitVersion(previousKit)) ? kit : undefined);
|
|
325
559
|
if (target === undefined) {
|
|
326
560
|
kept.push(`package.json keeps ${name}@${current}: ${product.packageName}@${release.version} does not name it`);
|
|
327
561
|
continue;
|
|
@@ -329,7 +563,9 @@ export async function upgradeProject(product: UpgradingProduct, requested?: stri
|
|
|
329
563
|
if (current === target) continue;
|
|
330
564
|
declared[name] = target;
|
|
331
565
|
packagesChanged = true;
|
|
332
|
-
changed.push(
|
|
566
|
+
changed.push(current.startsWith('file:')
|
|
567
|
+
? `package.json ${section}: ${name} was the local tarball ${current}; it is now ${target} from the registry`
|
|
568
|
+
: `package.json ${section}: ${name} ${current} -> ${target}`);
|
|
333
569
|
}
|
|
334
570
|
}
|
|
335
571
|
|
|
@@ -363,19 +599,49 @@ export async function upgradeProject(product: UpgradingProduct, requested?: stri
|
|
|
363
599
|
|
|
364
600
|
// A checkout's project links the checkout's own install, which already holds every kit
|
|
365
601
|
// package; `npm install` there would write into the checkout (`add-play`'s same rule).
|
|
366
|
-
const linked =
|
|
602
|
+
const linked = product.linksNodeModules === true
|
|
603
|
+
|| (() => { try { return lstatSync(join(project, 'node_modules')).isSymbolicLink(); } catch { return false; } })();
|
|
367
604
|
const locks = packagesChanged && !linked
|
|
368
605
|
? (await Promise.all([join(project, 'package-lock.json'), join(project, 'node_modules', '.package-lock.json')]
|
|
369
606
|
.map(lockWithoutVolter))).filter((lock): lock is NonNullable<typeof lock> => lock !== null)
|
|
370
607
|
: [];
|
|
371
608
|
if (locks.length > 0) changed.push('package-lock.json: the old @volter rows dropped, so npm install resolves the new ones');
|
|
372
609
|
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
610
|
+
// The folders this run makes for the moved files are removed again if the write fails, so nothing of it is left.
|
|
611
|
+
const madeDirs: string[] = [];
|
|
612
|
+
for (const file of textFiles) {
|
|
613
|
+
if (!file.created) continue;
|
|
614
|
+
const chain: string[] = [];
|
|
615
|
+
for (let dir = dirname(file.path); !existsSync(dir) && dir !== dirname(dir); dir = dirname(dir)) chain.unshift(dir);
|
|
616
|
+
for (const dir of chain) { await mkdir(dir); madeDirs.push(dir); }
|
|
617
|
+
}
|
|
618
|
+
try {
|
|
619
|
+
await writeAll([
|
|
620
|
+
...(packagesChanged ? [{ path: packagePath, content: jsonLayout(packageRaw)(pkg), original: packageRaw }] : []),
|
|
621
|
+
...(pinChanged ? [{ path: manifestPath, content: jsonLayout(manifestRaw)(manifest), original: manifestRaw }] : []),
|
|
622
|
+
...textFiles,
|
|
623
|
+
...locks,
|
|
624
|
+
]);
|
|
625
|
+
} catch (error) {
|
|
626
|
+
for (const dir of madeDirs.reverse()) await rmdir(dir).catch(() => undefined);
|
|
627
|
+
throw error;
|
|
628
|
+
}
|
|
629
|
+
// The moved scripts' new copies are in place; only then do the old ones go, and a moved folder's other files
|
|
630
|
+
// (data, a capability stamp) follow as they are. The project is upgraded by now, so a file that cannot be moved or
|
|
631
|
+
// removed (open in another program) is said, not thrown.
|
|
632
|
+
const failed: string[] = [];
|
|
633
|
+
const leftover = (path: string, error: unknown) => (failed.push(path),
|
|
634
|
+
kept.push(`${projectPath(path)} could not be removed (${error instanceof Error ? error.message : String(error)}); the editor does not read it, so delete it by hand`));
|
|
635
|
+
for (const path of leftBehind) await rm(path).catch((error: unknown) => leftover(path, error));
|
|
636
|
+
for (const move of moves) {
|
|
637
|
+
for (const path of await allFiles(move.from)) {
|
|
638
|
+
if (leftBehind.includes(path) || stranded.includes(path)) continue;
|
|
639
|
+
const to = movedPath(path);
|
|
640
|
+
await mkdir(dirname(to), { recursive: true });
|
|
641
|
+
await rename(path, to).catch((error: unknown) => leftover(path, error));
|
|
642
|
+
}
|
|
643
|
+
if (!move.from.endsWith('.ts') && ![...failed, ...stranded].some(path => path.startsWith(move.from + sep))) await rm(move.from, { recursive: true, force: true }).catch(() => undefined);
|
|
644
|
+
}
|
|
379
645
|
for (const line of warnings) console.warn(`! ${line}`);
|
|
380
646
|
|
|
381
647
|
console.log(changed.length > 0
|