@openfairygui/functions 0.2.0-alpha.7 → 0.2.0
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/README.md +52 -6
- package/dist/atlas-C6tbl7nn.d.ts +193 -0
- package/dist/atlas-CHsu2Y8i.d.cts +193 -0
- package/dist/index.cjs +17 -3603
- package/dist/index.d.cts +5 -294
- package/dist/index.d.ts +5 -294
- package/dist/index.js +4 -3595
- package/dist/node.cjs +256 -0
- package/dist/node.d.cts +36 -0
- package/dist/node.d.ts +36 -0
- package/dist/node.js +254 -0
- package/dist/publish-CykUJfVa.cjs +3269 -0
- package/dist/publish-DXoaC1Nl.js +3174 -0
- package/dist/restore-BQp01WY3.js +914 -0
- package/dist/restore-BeWaJNjR.d.cts +288 -0
- package/dist/restore-CEywQUHz.cjs +919 -0
- package/dist/restore-Dh0-Nvms.d.ts +288 -0
- package/dist/uam-transaction.cjs +29 -14
- package/dist/uam-transaction.d.cts +2 -1
- package/dist/uam-transaction.d.ts +2 -1
- package/dist/uam-transaction.js +30 -16
- package/dist/web.cjs +440 -0
- package/dist/web.d.cts +44 -0
- package/dist/web.d.ts +44 -0
- package/dist/web.js +439 -0
- package/package.json +29 -4
- package/src/adapters/node/plugins.ts +82 -0
- package/src/adapters/node/publish.ts +130 -0
- package/src/adapters/node/restore.ts +187 -0
- package/src/adapters/web/publish.ts +196 -0
- package/src/adapters/web/raster.ts +421 -0
- package/src/atlas/font.ts +95 -0
- package/src/atlas/inputs.ts +445 -0
- package/src/atlas/jta.ts +157 -0
- package/src/atlas/packing.ts +762 -0
- package/src/atlas.ts +129 -1221
- package/src/codegen.ts +108 -82
- package/src/index.ts +43 -3
- package/src/node.ts +8 -0
- package/src/path-utils.ts +40 -0
- package/src/plugins/types.ts +56 -0
- package/src/publish/contracts.ts +80 -0
- package/src/publish/external-resources.ts +117 -0
- package/src/publish/options.ts +180 -0
- package/src/publish/package-context.ts +608 -0
- package/src/publish/resource-references.ts +210 -0
- package/src/publish.ts +327 -975
- package/src/restore-internals/font.ts +100 -0
- package/src/restore-internals/movie-clip.ts +104 -0
- package/src/restore-internals/output-transaction.ts +124 -0
- package/src/restore.ts +122 -311
- package/src/shared-types.ts +4 -8
- package/src/uam-transaction.ts +34 -17
- package/src/utils.ts +28 -0
- package/src/web.ts +11 -0
package/src/restore.ts
CHANGED
|
@@ -3,10 +3,22 @@ import {
|
|
|
3
3
|
type Document,
|
|
4
4
|
type FileSystem,
|
|
5
5
|
generateId,
|
|
6
|
+
type Package,
|
|
6
7
|
ProjectType,
|
|
7
8
|
ProjectWriter,
|
|
8
|
-
type Package,
|
|
9
9
|
} from '@openfairygui/core';
|
|
10
|
+
import {
|
|
11
|
+
assertRestoreOutputDir,
|
|
12
|
+
basename,
|
|
13
|
+
commitRestoreOutput,
|
|
14
|
+
createRestoreStagingDir,
|
|
15
|
+
isPathWithin,
|
|
16
|
+
normalizeRestoreOutputDir,
|
|
17
|
+
resolveOutputProjectPath,
|
|
18
|
+
trimTrailingSlashes,
|
|
19
|
+
} from './restore-internals/output-transaction.js';
|
|
20
|
+
import { serializeFont, type RestorableFontGlyph } from './restore-internals/font.js';
|
|
21
|
+
import { serializeMovieClip, type RestorableMovieFrame } from './restore-internals/movie-clip.js';
|
|
10
22
|
|
|
11
23
|
export interface RestoreImageCropInput {
|
|
12
24
|
sourcePath: string;
|
|
@@ -45,7 +57,8 @@ export interface RestoreFileSystem extends Pick<FileSystem, 'readFile' | 'readFi
|
|
|
45
57
|
readdir(path: string): Promise<string[]>;
|
|
46
58
|
isFile(path: string): Promise<boolean>;
|
|
47
59
|
resolvePath(path: string): string | Promise<string>;
|
|
48
|
-
rm
|
|
60
|
+
rm(path: string, options?: { recursive?: boolean; force?: boolean }): Promise<void>;
|
|
61
|
+
rename(from: string, to: string): Promise<void>;
|
|
49
62
|
}
|
|
50
63
|
|
|
51
64
|
export interface RestoreOptions {
|
|
@@ -119,29 +132,6 @@ interface RestorableSprite {
|
|
|
119
132
|
getOriginalHeight(): number;
|
|
120
133
|
}
|
|
121
134
|
|
|
122
|
-
interface RestorableMovieFrame {
|
|
123
|
-
getRectX(): number;
|
|
124
|
-
getRectY(): number;
|
|
125
|
-
getRectWidth(): number;
|
|
126
|
-
getRectHeight(): number;
|
|
127
|
-
getAddDelay(): number;
|
|
128
|
-
getSpriteId(): string;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
interface RestorableFontGlyph {
|
|
132
|
-
getAdvance(): number;
|
|
133
|
-
getChannel(): number;
|
|
134
|
-
getChar(): string;
|
|
135
|
-
getCharId(): number;
|
|
136
|
-
getHeight(): number;
|
|
137
|
-
getImg(): string;
|
|
138
|
-
getWidth(): number;
|
|
139
|
-
getX(): number;
|
|
140
|
-
getXOffset(): number;
|
|
141
|
-
getY(): number;
|
|
142
|
-
getYOffset(): number;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
135
|
type RestorableFontResource = RestorableResource & {
|
|
146
136
|
listGlyphs(): RestorableFontGlyph[];
|
|
147
137
|
getBranch?(): string;
|
|
@@ -163,9 +153,6 @@ interface RestorableDisplayObject {
|
|
|
163
153
|
setFont?(font: string): unknown;
|
|
164
154
|
}
|
|
165
155
|
|
|
166
|
-
const JTA_FILE_MARK = 'yytou';
|
|
167
|
-
const JTA_VERSION = 102;
|
|
168
|
-
const JTA_DEFAULT_FPS = 24;
|
|
169
156
|
const TRANSPARENT_PNG_1X1 = Uint8Array.from([
|
|
170
157
|
137, 80, 78, 71, 13, 10, 26, 10,
|
|
171
158
|
0, 0, 0, 13, 73, 72, 68, 82,
|
|
@@ -178,10 +165,23 @@ const TRANSPARENT_PNG_1X1 = Uint8Array.from([
|
|
|
178
165
|
66, 96, 130,
|
|
179
166
|
]);
|
|
180
167
|
|
|
168
|
+
function assertSafeRestoreSegment(value: string, label: string): void {
|
|
169
|
+
if (!value || value === '.' || value === '..' || value.includes('\0') || /[\\/:]/u.test(value)) {
|
|
170
|
+
throw new Error(`restore: Invalid ${label} "${value}".`);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
181
174
|
function normalizeVirtualPath(path: string | undefined): string {
|
|
182
|
-
const
|
|
183
|
-
if (!
|
|
184
|
-
|
|
175
|
+
const raw = (path ?? '').trim();
|
|
176
|
+
if (!raw || raw === '/') return '';
|
|
177
|
+
if (raw.includes('\0') || raw.startsWith('\\') || raw.startsWith('//') || /^[a-z]:/iu.test(raw)) {
|
|
178
|
+
throw new Error(`restore: Invalid resource path "${raw}".`);
|
|
179
|
+
}
|
|
180
|
+
const segments = raw.replace(/\\/g, '/').split('/').filter(Boolean);
|
|
181
|
+
if (segments.some((segment) => segment === '.' || segment === '..' || segment.includes(':'))) {
|
|
182
|
+
throw new Error(`restore: Invalid resource path "${raw}".`);
|
|
183
|
+
}
|
|
184
|
+
return segments.join('/');
|
|
185
185
|
}
|
|
186
186
|
|
|
187
187
|
function resourceFileName(resource: RestorableResource): string {
|
|
@@ -338,59 +338,6 @@ function findImageResource(pkg: Package, itemId: string): RestorableResource | n
|
|
|
338
338
|
}) ?? null;
|
|
339
339
|
}
|
|
340
340
|
|
|
341
|
-
function fontGlyphCharId(glyph: RestorableFontGlyph): number {
|
|
342
|
-
const charId = glyph.getCharId();
|
|
343
|
-
if (charId > 0) return charId;
|
|
344
|
-
const char = glyph.getChar();
|
|
345
|
-
return char ? (char.codePointAt(0) ?? 0) : 0;
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
function scaledFrameDelay(milliseconds: number): number {
|
|
349
|
-
return milliseconds <= 0 ? 0 : Math.max(1, Math.round(milliseconds / (1000 / JTA_DEFAULT_FPS)));
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
function jtaSpeed(interval: number): number {
|
|
353
|
-
return interval <= 0 ? 1 : Math.max(1, Math.round(interval / (1000 / JTA_DEFAULT_FPS)));
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
function writeInt16(value: number): Uint8Array {
|
|
357
|
-
const data = new Uint8Array(2);
|
|
358
|
-
new DataView(data.buffer).setInt16(0, value);
|
|
359
|
-
return data;
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
function writeUint16(value: number): Uint8Array {
|
|
363
|
-
const data = new Uint8Array(2);
|
|
364
|
-
new DataView(data.buffer).setUint16(0, value);
|
|
365
|
-
return data;
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
function writeInt32(value: number): Uint8Array {
|
|
369
|
-
const data = new Uint8Array(4);
|
|
370
|
-
new DataView(data.buffer).setInt32(0, value);
|
|
371
|
-
return data;
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
function writeByte(value: number): Uint8Array {
|
|
375
|
-
return new Uint8Array([value & 0xff]);
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
function concatBytes(chunks: Uint8Array[]): Uint8Array {
|
|
379
|
-
const length = chunks.reduce((total, chunk) => total + chunk.byteLength, 0);
|
|
380
|
-
const data = new Uint8Array(length);
|
|
381
|
-
let offset = 0;
|
|
382
|
-
for (const chunk of chunks) {
|
|
383
|
-
data.set(chunk, offset);
|
|
384
|
-
offset += chunk.byteLength;
|
|
385
|
-
}
|
|
386
|
-
return data;
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
function encodeJtaUtf(value: string): Uint8Array {
|
|
390
|
-
const bytes = new TextEncoder().encode(value);
|
|
391
|
-
return concatBytes([writeUint16(bytes.byteLength), bytes]);
|
|
392
|
-
}
|
|
393
|
-
|
|
394
341
|
function isPublishedBinaryFile(fileName: string): boolean {
|
|
395
342
|
return /_fui\.bytes$/i.test(fileName) || /\.fui$/i.test(fileName) || /\.bin$/i.test(fileName);
|
|
396
343
|
}
|
|
@@ -401,135 +348,18 @@ function inferPackageName(fileName: string): string {
|
|
|
401
348
|
return fileName.replace(/\.bin$/i, '');
|
|
402
349
|
}
|
|
403
350
|
|
|
404
|
-
function trimTrailingSlashes(value: string): string {
|
|
405
|
-
return value.replace(/[/\\]+$/, '');
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
function normalizeComparablePath(value: string): string {
|
|
409
|
-
const normalized = trimTrailingSlashes(value).replace(/\\/g, '/');
|
|
410
|
-
const driveMatch = normalized.match(/^([a-z]:)(?:\/(.*))?$/i);
|
|
411
|
-
const drivePrefix = driveMatch?.[1].toLowerCase() ?? '';
|
|
412
|
-
const remainder = driveMatch ? (driveMatch[2] ?? '') : normalized;
|
|
413
|
-
const hasRoot = driveMatch ? true : remainder.startsWith('/');
|
|
414
|
-
const rawSegments = remainder.split('/').filter((segment) => segment.length > 0);
|
|
415
|
-
const segments: string[] = [];
|
|
416
|
-
|
|
417
|
-
for (const segment of rawSegments) {
|
|
418
|
-
if (segment === '.') continue;
|
|
419
|
-
if (segment === '..') {
|
|
420
|
-
if (segments.length > 0 && segments[segments.length - 1] !== '..') {
|
|
421
|
-
segments.pop();
|
|
422
|
-
} else if (!hasRoot) {
|
|
423
|
-
segments.push('..');
|
|
424
|
-
}
|
|
425
|
-
continue;
|
|
426
|
-
}
|
|
427
|
-
segments.push(segment);
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
const joined = segments.join('/');
|
|
431
|
-
const comparable = drivePrefix
|
|
432
|
-
? `${drivePrefix}/${joined}`.replace(/\/$/, '')
|
|
433
|
-
: hasRoot
|
|
434
|
-
? `/${joined}`.replace(/\/$/, '')
|
|
435
|
-
: joined || '.';
|
|
436
|
-
// Restore prefers a conservative same-directory guard: false positives are safer than
|
|
437
|
-
// missing a Windows-style case-only path alias and deleting the source publish dir.
|
|
438
|
-
return comparable.toLowerCase();
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
function dirname(filePath: string): string {
|
|
442
|
-
const trimmed = trimTrailingSlashes(filePath);
|
|
443
|
-
const match = trimmed.match(/^(.*)[/\\][^/\\]+$/);
|
|
444
|
-
return match?.[1] ?? '';
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
function basename(filePath: string): string {
|
|
448
|
-
const trimmed = trimTrailingSlashes(filePath);
|
|
449
|
-
const match = trimmed.match(/([^/\\]+)$/);
|
|
450
|
-
return match?.[1] ?? '';
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
function resolveOutputProjectPath(output: string, fs: Pick<RestoreFileSystem, 'join'>): string {
|
|
454
|
-
if (/\.fairy$/i.test(output)) return output;
|
|
455
|
-
const normalizedOutput = trimTrailingSlashes(output);
|
|
456
|
-
const projectName = basename(normalizedOutput) || 'Restored';
|
|
457
|
-
return fs.join(normalizedOutput, `${projectName}.fairy`);
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
async function prepareRestoreOutputDir(
|
|
461
|
-
inputDir: string,
|
|
462
|
-
outputDir: string,
|
|
463
|
-
outputProjectPath: string,
|
|
464
|
-
fs: RestoreFileSystem,
|
|
465
|
-
force: boolean,
|
|
466
|
-
outputIsProjectFile: boolean,
|
|
467
|
-
): Promise<void> {
|
|
468
|
-
const [resolvedInputDir, resolvedOutputDir] = await Promise.all([
|
|
469
|
-
Promise.resolve(fs.resolvePath(inputDir)),
|
|
470
|
-
Promise.resolve(fs.resolvePath(outputDir)),
|
|
471
|
-
]);
|
|
472
|
-
if (normalizeComparablePath(resolvedInputDir) === normalizeComparablePath(resolvedOutputDir)) {
|
|
473
|
-
throw new Error('Restore output directory must be different from the published input directory.');
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
if (outputIsProjectFile) {
|
|
477
|
-
if (!(await fs.exists(outputDir))) {
|
|
478
|
-
await fs.mkdir(outputDir);
|
|
479
|
-
return;
|
|
480
|
-
}
|
|
481
|
-
try {
|
|
482
|
-
await fs.readdir(outputDir);
|
|
483
|
-
} catch {
|
|
484
|
-
throw new Error(`Restore output path is not a directory: ${outputDir}`);
|
|
485
|
-
}
|
|
486
|
-
|
|
487
|
-
if (!(await fs.exists(outputProjectPath))) return;
|
|
488
|
-
if (!force) {
|
|
489
|
-
throw new Error(`Restore output file already exists: ${outputProjectPath}. Use --force to overwrite it.`);
|
|
490
|
-
}
|
|
491
|
-
if (!fs.rm) {
|
|
492
|
-
throw new Error('Restore output file already exists and the provided fs does not support rm(...).');
|
|
493
|
-
}
|
|
494
|
-
await fs.rm(outputProjectPath, { recursive: true, force: true });
|
|
495
|
-
return;
|
|
496
|
-
}
|
|
497
|
-
|
|
498
|
-
const exists = await fs.exists(outputDir);
|
|
499
|
-
if (!exists) {
|
|
500
|
-
await fs.mkdir(outputDir);
|
|
501
|
-
return;
|
|
502
|
-
}
|
|
503
|
-
|
|
504
|
-
let entries: string[];
|
|
505
|
-
try {
|
|
506
|
-
entries = await fs.readdir(outputDir);
|
|
507
|
-
} catch {
|
|
508
|
-
throw new Error(`Restore output path is not a directory: ${outputDir}`);
|
|
509
|
-
}
|
|
510
|
-
|
|
511
|
-
if (entries.length === 0) return;
|
|
512
|
-
if (!force) {
|
|
513
|
-
throw new Error(`Restore output directory is not empty: ${outputDir}. Use --force to overwrite it.`);
|
|
514
|
-
}
|
|
515
|
-
if (!fs.rm) {
|
|
516
|
-
throw new Error('Restore output directory is not empty and the provided fs does not support rm(...).');
|
|
517
|
-
}
|
|
518
|
-
await fs.rm(outputDir, { recursive: true, force: true });
|
|
519
|
-
await fs.mkdir(outputDir);
|
|
520
|
-
}
|
|
521
|
-
|
|
522
351
|
export async function restore(options: RestoreOptions): Promise<RestoreResult> {
|
|
523
352
|
const sourceDir = trimTrailingSlashes(options.inputDir);
|
|
524
|
-
const
|
|
525
|
-
const outputProjectPath = resolveOutputProjectPath(
|
|
526
|
-
|
|
527
|
-
await prepareRestoreOutputDir(sourceDir, outputDir, outputProjectPath, options.fs, options.force === true, outputIsProjectFile);
|
|
353
|
+
const outputDir = normalizeRestoreOutputDir(options.output);
|
|
354
|
+
const outputProjectPath = resolveOutputProjectPath(outputDir, options.fs);
|
|
355
|
+
await assertRestoreOutputDir(sourceDir, outputDir, options.fs, options.force === true);
|
|
528
356
|
|
|
529
357
|
const packageFilter = options.packages?.length ? new Set(options.packages) : null;
|
|
530
|
-
const
|
|
358
|
+
const binaryNames = (await options.fs.readdir(sourceDir))
|
|
531
359
|
.filter((name) => isPublishedBinaryFile(name))
|
|
532
|
-
.filter((name) => !packageFilter || packageFilter.has(inferPackageName(name)))
|
|
360
|
+
.filter((name) => !packageFilter || packageFilter.has(inferPackageName(name)));
|
|
361
|
+
for (const binaryName of binaryNames) assertSafeRestoreSegment(binaryName, 'published binary file name');
|
|
362
|
+
const candidateBinaryPaths = binaryNames
|
|
533
363
|
.map((name) => options.fs.join(sourceDir, name))
|
|
534
364
|
.sort((left, right) => left.localeCompare(right));
|
|
535
365
|
const binaryPaths = (await Promise.all(
|
|
@@ -543,7 +373,7 @@ export async function restore(options: RestoreOptions): Promise<RestoreResult> {
|
|
|
543
373
|
}
|
|
544
374
|
|
|
545
375
|
const restorer = new RestoreWorkflow(options.fs);
|
|
546
|
-
|
|
376
|
+
const document = await restorer.prepare({
|
|
547
377
|
binaryPaths,
|
|
548
378
|
sourceDir,
|
|
549
379
|
outputProjectPath,
|
|
@@ -551,6 +381,26 @@ export async function restore(options: RestoreOptions): Promise<RestoreResult> {
|
|
|
551
381
|
cropImage: options.cropImage,
|
|
552
382
|
extractImage: options.extractImage,
|
|
553
383
|
});
|
|
384
|
+
const stagingDir = await createRestoreStagingDir(outputDir, options.fs);
|
|
385
|
+
const stagingProjectPath = options.fs.join(stagingDir, basename(outputProjectPath));
|
|
386
|
+
const warnings: string[] = [];
|
|
387
|
+
try {
|
|
388
|
+
await restorer.write(document, {
|
|
389
|
+
binaryPaths,
|
|
390
|
+
sourceDir,
|
|
391
|
+
outputProjectPath: stagingProjectPath,
|
|
392
|
+
projectType: options.projectType,
|
|
393
|
+
cropImage: options.cropImage,
|
|
394
|
+
extractImage: options.extractImage,
|
|
395
|
+
}, warnings);
|
|
396
|
+
const cleanupWarning = await commitRestoreOutput(stagingDir, outputDir, options.fs);
|
|
397
|
+
if (cleanupWarning) warnings.push(cleanupWarning);
|
|
398
|
+
} catch (error) {
|
|
399
|
+
await options.fs.rm(stagingDir, { recursive: true, force: true }).catch(() => undefined);
|
|
400
|
+
throw error;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
return { document, projectPath: outputProjectPath, warnings };
|
|
554
404
|
}
|
|
555
405
|
|
|
556
406
|
class RestoreWorkflow {
|
|
@@ -560,13 +410,14 @@ class RestoreWorkflow {
|
|
|
560
410
|
this._fs = fs;
|
|
561
411
|
}
|
|
562
412
|
|
|
563
|
-
async
|
|
564
|
-
const warnings: string[] = [];
|
|
413
|
+
async prepare(options: RestoreExecutionOptions): Promise<Document> {
|
|
565
414
|
const reader = new BinaryReader(this._fs);
|
|
566
415
|
const doc = await reader.readMany(options.binaryPaths);
|
|
416
|
+
this._assertDocumentPaths(doc);
|
|
567
417
|
this._initializeProjectDefaults(doc, options.projectType);
|
|
568
418
|
this._initializeImageFileNames(doc);
|
|
569
419
|
this._initializeLooseResourceFileNames(doc);
|
|
420
|
+
this._assertDocumentPaths(doc);
|
|
570
421
|
await this._synthesizeLooseSkeletonResources(doc, options.sourceDir);
|
|
571
422
|
this._initializeRestoredResourceRelations(doc);
|
|
572
423
|
this._initializePublishedFontTextureIds(doc);
|
|
@@ -575,16 +426,30 @@ class RestoreWorkflow {
|
|
|
575
426
|
this._initializePublishedTextFontResources(doc);
|
|
576
427
|
this._initializeDisplayObjectFileNames(doc);
|
|
577
428
|
this._initializePublishedFontDefaults(doc);
|
|
429
|
+
this._assertDocumentPaths(doc);
|
|
430
|
+
return doc;
|
|
431
|
+
}
|
|
578
432
|
|
|
433
|
+
async write(doc: Document, options: RestoreExecutionOptions, warnings: string[]): Promise<void> {
|
|
579
434
|
const writer = new ProjectWriter(this._fs);
|
|
580
435
|
await writer.write(doc, options.outputProjectPath);
|
|
581
436
|
await this._restoreAssets(doc, options, warnings);
|
|
437
|
+
}
|
|
582
438
|
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
439
|
+
private _assertDocumentPaths(doc: Document): void {
|
|
440
|
+
for (const pkg of doc.getRoot().listPackages()) {
|
|
441
|
+
assertSafeRestoreSegment(pkg.getName(), 'package name');
|
|
442
|
+
assertSafeRestoreSegment(pkg.getPublishName() || pkg.getName(), 'package publish name');
|
|
443
|
+
for (const resource of pkg.listResources() as RestorableResource[]) {
|
|
444
|
+
normalizeVirtualPath(resource.getPath?.());
|
|
445
|
+
const branch = resource.getBranch?.() ?? '';
|
|
446
|
+
if (branch) assertSafeRestoreSegment(branch, 'branch name');
|
|
447
|
+
const fileName = resourceFileName(resource);
|
|
448
|
+
if (fileName) assertSafeRestoreSegment(fileName, 'resource file name');
|
|
449
|
+
const publishedFileName = resourcePublishedFileName(resource);
|
|
450
|
+
if (publishedFileName) assertSafeRestoreSegment(publishedFileName, 'published resource file name');
|
|
451
|
+
}
|
|
452
|
+
}
|
|
588
453
|
}
|
|
589
454
|
|
|
590
455
|
private _initializeProjectDefaults(doc: Document, projectType?: number): void {
|
|
@@ -601,6 +466,16 @@ class RestoreWorkflow {
|
|
|
601
466
|
common: {},
|
|
602
467
|
adaptation: {},
|
|
603
468
|
});
|
|
469
|
+
for (const pkg of doc.getRoot().listPackages()) {
|
|
470
|
+
pkg.setSourceAtlasSettings({
|
|
471
|
+
...pkg.getSourceAtlasSettings(),
|
|
472
|
+
atlases: pkg.listAtlases().map((atlas) => ({
|
|
473
|
+
index: atlas.getIndex(),
|
|
474
|
+
name: atlas.getIndex() === 0 ? 'Default' : atlas.getName(),
|
|
475
|
+
compression: false,
|
|
476
|
+
})),
|
|
477
|
+
});
|
|
478
|
+
}
|
|
604
479
|
}
|
|
605
480
|
|
|
606
481
|
private _initializeImageFileNames(doc: Document): void {
|
|
@@ -757,9 +632,16 @@ class RestoreWorkflow {
|
|
|
757
632
|
): Promise<RestorableResource | null> {
|
|
758
633
|
const resources = pkg.listResources() as RestorableResource[];
|
|
759
634
|
const existing = this._findResourceByFile(resources, owner, 'ImageResource', fileName);
|
|
760
|
-
if (existing) return existing;
|
|
761
635
|
const sourcePath = await this._resolveLooseSourceFile(pkg, sourceDir, fileName);
|
|
762
|
-
if (!sourcePath) return null;
|
|
636
|
+
if (!sourcePath) return existing ?? null;
|
|
637
|
+
if (existing) {
|
|
638
|
+
existing.setExtras?.({
|
|
639
|
+
...(existing.getExtras?.() ?? {}),
|
|
640
|
+
_publishedFile: fileBaseName(sourcePath),
|
|
641
|
+
_restoreAsLooseImage: true,
|
|
642
|
+
});
|
|
643
|
+
return existing;
|
|
644
|
+
}
|
|
763
645
|
const resource = doc.createImageResource(stripExtension(fileName));
|
|
764
646
|
resource
|
|
765
647
|
.setId(generateId())
|
|
@@ -772,7 +654,7 @@ class RestoreWorkflow {
|
|
|
772
654
|
...(resource.getExtras?.() ?? {}),
|
|
773
655
|
_publishedFile: fileBaseName(sourcePath),
|
|
774
656
|
_suppressPackageSize: true,
|
|
775
|
-
|
|
657
|
+
_restoreAsLooseImage: true,
|
|
776
658
|
});
|
|
777
659
|
pkg.addResource(resource);
|
|
778
660
|
return resource as RestorableResource;
|
|
@@ -1022,8 +904,8 @@ class RestoreWorkflow {
|
|
|
1022
904
|
warnings: string[],
|
|
1023
905
|
): Promise<void> {
|
|
1024
906
|
for (const resource of pkg.listResources() as RestorableResource[]) {
|
|
1025
|
-
const
|
|
1026
|
-
if (!['SoundResource', 'MiscResource', 'SpineResource', 'DragonBonesResource'].includes(resource.propertyType) && !
|
|
907
|
+
const restoreAsLooseImage = resource.getExtras?.()?._restoreAsLooseImage === true;
|
|
908
|
+
if (!['SoundResource', 'MiscResource', 'SpineResource', 'DragonBonesResource'].includes(resource.propertyType) && !restoreAsLooseImage) {
|
|
1027
909
|
continue;
|
|
1028
910
|
}
|
|
1029
911
|
const fileName = resourceFileName(resource);
|
|
@@ -1065,52 +947,7 @@ class RestoreWorkflow {
|
|
|
1065
947
|
|
|
1066
948
|
const outputPath = this._resourceOutputPath(outputProjectPath, pkg, resource, fileName);
|
|
1067
949
|
await this._mkdirForFile(outputPath);
|
|
1068
|
-
await this._fs.writeFile(outputPath,
|
|
1069
|
-
}
|
|
1070
|
-
|
|
1071
|
-
private _serializeFont(pkg: Package, resource: RestorableResource, glyphs: RestorableFontGlyph[]): string {
|
|
1072
|
-
const isTtf = resource.getTtf?.() === true;
|
|
1073
|
-
const lines = isTtf
|
|
1074
|
-
? this._serializeTtfFontHeader(pkg, resource, glyphs)
|
|
1075
|
-
: [
|
|
1076
|
-
'info creator=UIBuilder',
|
|
1077
|
-
`common lineHeight=${resource.getLineHeight?.() ?? 0}`,
|
|
1078
|
-
];
|
|
1079
|
-
|
|
1080
|
-
for (const glyph of glyphs) {
|
|
1081
|
-
const charId = fontGlyphCharId(glyph);
|
|
1082
|
-
if (isTtf) {
|
|
1083
|
-
lines.push(
|
|
1084
|
-
`char id=${charId} x=${glyph.getX()} y=${glyph.getY()} width=${glyph.getWidth()} height=${glyph.getHeight()} `
|
|
1085
|
-
+ `xoffset=${glyph.getXOffset()} yoffset=${glyph.getYOffset()} xadvance=${glyph.getAdvance()} page=0 chnl=${glyph.getChannel()}`,
|
|
1086
|
-
);
|
|
1087
|
-
} else {
|
|
1088
|
-
lines.push(
|
|
1089
|
-
`char id=${charId} img=${glyph.getImg()} xoffset=${glyph.getXOffset()} yoffset=${glyph.getYOffset()} xadvance=${glyph.getAdvance()}`,
|
|
1090
|
-
);
|
|
1091
|
-
}
|
|
1092
|
-
}
|
|
1093
|
-
|
|
1094
|
-
return `${lines.join('\n')}\n`;
|
|
1095
|
-
}
|
|
1096
|
-
|
|
1097
|
-
private _serializeTtfFontHeader(pkg: Package, resource: RestorableResource, glyphs: RestorableFontGlyph[]): string[] {
|
|
1098
|
-
const fileName = resourceFileName(resource);
|
|
1099
|
-
const face = stripExtension(fileName) || resource.getName?.() || 'Font';
|
|
1100
|
-
const lineHeight = resource.getLineHeight?.() ?? 0;
|
|
1101
|
-
const fontSize = resource.getFontSize?.() ?? lineHeight;
|
|
1102
|
-
const textureId = resource.getTextureId?.() ?? '';
|
|
1103
|
-
const textureResource = textureId ? pkg.getResourceById(textureId) as RestorableResource | null : null;
|
|
1104
|
-
const textureName = textureResource ? resourceFileName(textureResource) : `${face}_atlas.png`;
|
|
1105
|
-
const scaleW = textureResource?.getWidth?.() ?? 256;
|
|
1106
|
-
const scaleH = textureResource?.getHeight?.() ?? 256;
|
|
1107
|
-
const base = Math.max(Math.min(fontSize, lineHeight) - 6, 0);
|
|
1108
|
-
return [
|
|
1109
|
-
`info face="${face}" size=${fontSize} bold=0 italic=0 charset="" unicode=1 stretchH=100 smooth=1 aa=1 padding=0,0,0,0 spacing=1,1 outline=0`,
|
|
1110
|
-
`common lineHeight=${lineHeight} base=${base} scaleW=${scaleW} scaleH=${scaleH} pages=1 packed=0 alphaChnl=${resource.getTint?.() ? 1 : 0} redChnl=0 greenChnl=0 blueChnl=0`,
|
|
1111
|
-
`page id=0 file="${textureName}"`,
|
|
1112
|
-
`chars count=${glyphs.length}`,
|
|
1113
|
-
];
|
|
950
|
+
await this._fs.writeFile(outputPath, serializeFont(pkg, resource, glyphs));
|
|
1114
951
|
}
|
|
1115
952
|
|
|
1116
953
|
private async _writeMovieClipFile(
|
|
@@ -1157,7 +994,7 @@ class RestoreWorkflow {
|
|
|
1157
994
|
|
|
1158
995
|
const outputPath = this._resourceOutputPath(options.outputProjectPath, pkg, resource, fileName);
|
|
1159
996
|
await this._mkdirForFile(outputPath);
|
|
1160
|
-
await this._fs.writeFileRaw(outputPath,
|
|
997
|
+
await this._fs.writeFileRaw(outputPath, serializeMovieClip(resource, frames, textures));
|
|
1161
998
|
}
|
|
1162
999
|
|
|
1163
1000
|
private async _writeSyntheticFontGlyphImages(pkg: Package, outputProjectPath: string): Promise<void> {
|
|
@@ -1187,55 +1024,19 @@ class RestoreWorkflow {
|
|
|
1187
1024
|
return sprites;
|
|
1188
1025
|
}
|
|
1189
1026
|
|
|
1190
|
-
private _serializeMovieClip(
|
|
1191
|
-
resource: RestorableResource,
|
|
1192
|
-
frames: RestorableMovieFrame[],
|
|
1193
|
-
textures: Uint8Array[],
|
|
1194
|
-
): Uint8Array {
|
|
1195
|
-
const chunks: Uint8Array[] = [
|
|
1196
|
-
encodeJtaUtf(JTA_FILE_MARK),
|
|
1197
|
-
writeInt32(JTA_VERSION),
|
|
1198
|
-
writeByte(0),
|
|
1199
|
-
writeByte(0),
|
|
1200
|
-
writeByte(0),
|
|
1201
|
-
writeByte(0),
|
|
1202
|
-
writeUint16(0),
|
|
1203
|
-
writeUint16(0),
|
|
1204
|
-
writeUint16(resource.getWidth?.() ?? 0),
|
|
1205
|
-
writeUint16(resource.getHeight?.() ?? 0),
|
|
1206
|
-
writeByte(jtaSpeed(resource.getInterval?.() ?? 0)),
|
|
1207
|
-
writeByte(scaledFrameDelay(resource.getRepeatDelay?.() ?? 0)),
|
|
1208
|
-
writeByte(resource.getSwing?.() ? 1 : 0),
|
|
1209
|
-
writeInt16(frames.length),
|
|
1210
|
-
];
|
|
1211
|
-
|
|
1212
|
-
for (const [index, frame] of frames.entries()) {
|
|
1213
|
-
chunks.push(
|
|
1214
|
-
writeInt16(scaledFrameDelay(frame.getAddDelay())),
|
|
1215
|
-
writeInt16(frame.getRectX()),
|
|
1216
|
-
writeInt16(frame.getRectY()),
|
|
1217
|
-
writeInt16(frame.getRectWidth()),
|
|
1218
|
-
writeInt16(frame.getRectHeight()),
|
|
1219
|
-
writeInt16(textures[index]?.byteLength === 0 ? -1 : index),
|
|
1220
|
-
);
|
|
1221
|
-
}
|
|
1222
|
-
|
|
1223
|
-
chunks.push(writeInt16(textures.length));
|
|
1224
|
-
for (const texture of textures) {
|
|
1225
|
-
chunks.push(writeInt32(texture.byteLength), texture);
|
|
1226
|
-
}
|
|
1227
|
-
|
|
1228
|
-
return concatBytes(chunks);
|
|
1229
|
-
}
|
|
1230
|
-
|
|
1231
1027
|
private _sourceFileCandidates(pkg: Package, fileName: string, outputFileName = fileName): string[] {
|
|
1232
1028
|
const publishName = pkg.getPublishName() || pkg.getName();
|
|
1233
|
-
|
|
1029
|
+
assertSafeRestoreSegment(publishName, 'package publish name');
|
|
1030
|
+
assertSafeRestoreSegment(fileName, 'published source file name');
|
|
1031
|
+
assertSafeRestoreSegment(outputFileName, 'published source file name');
|
|
1032
|
+
const candidates = Array.from(new Set([
|
|
1234
1033
|
`${publishName}_${fileName}`,
|
|
1235
1034
|
fileName,
|
|
1236
1035
|
`${publishName}_${outputFileName}`,
|
|
1237
1036
|
outputFileName,
|
|
1238
1037
|
]));
|
|
1038
|
+
for (const candidate of candidates) assertSafeRestoreSegment(candidate, 'published source file name');
|
|
1039
|
+
return candidates;
|
|
1239
1040
|
}
|
|
1240
1041
|
|
|
1241
1042
|
private async _resolveLooseSourceFile(pkg: Package, sourceDir: string, outputFileName: string): Promise<string | null> {
|
|
@@ -1248,9 +1049,16 @@ class RestoreWorkflow {
|
|
|
1248
1049
|
}
|
|
1249
1050
|
|
|
1250
1051
|
private async _resolveSourceFile(sourceDir: string, candidates: string[]): Promise<string | null> {
|
|
1052
|
+
const resolvedSourceDir = await Promise.resolve(this._fs.resolvePath(sourceDir));
|
|
1251
1053
|
for (const candidate of candidates) {
|
|
1054
|
+
assertSafeRestoreSegment(candidate, 'published source file name');
|
|
1252
1055
|
const sourcePath = this._fs.join(sourceDir, candidate);
|
|
1253
|
-
if (await this._fs.isFile(sourcePath))
|
|
1056
|
+
if (!(await this._fs.isFile(sourcePath))) continue;
|
|
1057
|
+
const resolvedSourcePath = await Promise.resolve(this._fs.resolvePath(sourcePath));
|
|
1058
|
+
if (!isPathWithin(resolvedSourceDir, resolvedSourcePath)) {
|
|
1059
|
+
throw new Error(`restore: Published source file resolves outside the input directory: ${candidate}.`);
|
|
1060
|
+
}
|
|
1061
|
+
return resolvedSourcePath;
|
|
1254
1062
|
}
|
|
1255
1063
|
return null;
|
|
1256
1064
|
}
|
|
@@ -1263,6 +1071,9 @@ class RestoreWorkflow {
|
|
|
1263
1071
|
): string {
|
|
1264
1072
|
const basePath = this._fs.dirname(outputProjectPath);
|
|
1265
1073
|
const branch = resource.getBranch?.() ?? '';
|
|
1074
|
+
assertSafeRestoreSegment(pkg.getName(), 'package name');
|
|
1075
|
+
if (branch) assertSafeRestoreSegment(branch, 'branch name');
|
|
1076
|
+
assertSafeRestoreSegment(fileName, 'resource file name');
|
|
1266
1077
|
const assetsDir = branch ? `assets_${branch}` : 'assets';
|
|
1267
1078
|
const virtualPath = normalizeVirtualPath(resource.getPath?.());
|
|
1268
1079
|
const pkgDir = this._fs.join(basePath, assetsDir, pkg.getName());
|
package/src/shared-types.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { ProjectSettings, PublishSettings } from '@openfairygui/core';
|
|
2
|
+
|
|
3
|
+
export type { PublishFileSystem } from './publish/contracts.js';
|
|
2
4
|
|
|
3
5
|
export type ExtrasMap = Record<string, unknown>;
|
|
4
6
|
|
|
@@ -41,6 +43,7 @@ export interface PublishDependency {
|
|
|
41
43
|
|
|
42
44
|
export interface PackagePublishArtifactsExtras extends ExtrasMap {
|
|
43
45
|
publishedResourceIds?: string[];
|
|
46
|
+
exportedResourceIds?: string[];
|
|
44
47
|
publishedIncludeBranches?: boolean;
|
|
45
48
|
publishedEffectiveResourceIds?: Record<string, string>;
|
|
46
49
|
}
|
|
@@ -56,10 +59,3 @@ export interface HasOptionalSrc {
|
|
|
56
59
|
export interface HasOptionalUrl {
|
|
57
60
|
getUrl?(): string | undefined;
|
|
58
61
|
}
|
|
59
|
-
|
|
60
|
-
export type PublishFileSystem = Pick<FileSystem, 'join' | 'mkdir' | 'writeFileRaw'> & {
|
|
61
|
-
deleteFile?: (path: string) => Promise<void>;
|
|
62
|
-
exists?: FileSystem['exists'];
|
|
63
|
-
readdir?: FileSystem['readdir'];
|
|
64
|
-
readFileRaw?: FileSystem['readFileRaw'];
|
|
65
|
-
};
|
package/src/uam-transaction.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
applyUamTransaction,
|
|
3
|
+
applyUamTransactionAsync,
|
|
3
4
|
type UamProject,
|
|
4
5
|
UamTransactionError,
|
|
5
6
|
type UamTransactionErrorCode,
|
|
@@ -107,6 +108,24 @@ function mapTransactionDiagnostics(error: UamTransactionError): ApplyUamTransact
|
|
|
107
108
|
];
|
|
108
109
|
}
|
|
109
110
|
|
|
111
|
+
function transactionFailure(error: UamTransactionError): ApplyUamTransactionAppResult {
|
|
112
|
+
return {
|
|
113
|
+
ok: false,
|
|
114
|
+
error: {
|
|
115
|
+
code: error.code,
|
|
116
|
+
stage: mapTransactionErrorStage(error),
|
|
117
|
+
message: error.message,
|
|
118
|
+
opIndex: error.opIndex,
|
|
119
|
+
opId: error.opId,
|
|
120
|
+
opKind: error.opKind,
|
|
121
|
+
operationKind: error.opKind,
|
|
122
|
+
selector: error.selector,
|
|
123
|
+
issues: error.issues,
|
|
124
|
+
diagnostics: mapTransactionDiagnostics(error),
|
|
125
|
+
},
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
110
129
|
export function applyUamTransactionApp(input: ApplyUamTransactionAppInput): ApplyUamTransactionAppResult {
|
|
111
130
|
try {
|
|
112
131
|
return {
|
|
@@ -114,23 +133,21 @@ export function applyUamTransactionApp(input: ApplyUamTransactionAppInput): Appl
|
|
|
114
133
|
project: applyUamTransaction(input.project, input.operations),
|
|
115
134
|
};
|
|
116
135
|
} catch (error) {
|
|
117
|
-
if (error instanceof UamTransactionError)
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
};
|
|
133
|
-
}
|
|
136
|
+
if (error instanceof UamTransactionError) return transactionFailure(error);
|
|
137
|
+
throw error;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export async function applyUamTransactionAppAsync(
|
|
142
|
+
input: ApplyUamTransactionAppInput,
|
|
143
|
+
): Promise<ApplyUamTransactionAppResult> {
|
|
144
|
+
try {
|
|
145
|
+
return {
|
|
146
|
+
ok: true,
|
|
147
|
+
project: await applyUamTransactionAsync(input.project, input.operations),
|
|
148
|
+
};
|
|
149
|
+
} catch (error) {
|
|
150
|
+
if (error instanceof UamTransactionError) return transactionFailure(error);
|
|
134
151
|
throw error;
|
|
135
152
|
}
|
|
136
153
|
}
|