@openfairygui/functions 0.2.0-alpha.2 → 0.2.0-alpha.21
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 +48 -6
- package/dist/atlas-C6tbl7nn.d.ts +193 -0
- package/dist/atlas-CHsu2Y8i.d.cts +193 -0
- package/dist/index.cjs +16 -3603
- package/dist/index.d.cts +5 -294
- package/dist/index.d.ts +5 -294
- package/dist/index.js +3 -3594
- 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-BJ_eelME.js +3267 -0
- package/dist/publish-xFWT9Slz.cjs +3338 -0
- package/dist/restore-BW2xacB3.cjs +936 -0
- package/dist/restore-BeWaJNjR.d.cts +288 -0
- package/dist/restore-Clk62n0O.js +931 -0
- package/dist/restore-Dh0-Nvms.d.ts +288 -0
- package/dist/uam-transaction.cjs +44 -1
- package/dist/uam-transaction.d.cts +16 -1
- package/dist/uam-transaction.d.ts +16 -1
- package/dist/uam-transaction.js +44 -1
- package/dist/web.cjs +274 -0
- package/dist/web.d.cts +41 -0
- package/dist/web.d.ts +41 -0
- package/dist/web.js +273 -0
- package/package.json +28 -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 +159 -0
- package/src/adapters/web/raster.ts +251 -0
- package/src/atlas/font.ts +95 -0
- package/src/atlas/inputs.ts +515 -0
- package/src/atlas/jta.ts +211 -0
- package/src/atlas/packing.ts +767 -0
- package/src/atlas.ts +116 -1221
- package/src/codegen.ts +106 -67
- package/src/index.ts +43 -3
- package/src/node.ts +8 -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 +290 -968
- package/src/restore-internals/font.ts +100 -0
- package/src/restore-internals/movie-clip.ts +104 -0
- package/src/restore-internals/output-transaction.ts +164 -0
- package/src/restore.ts +112 -311
- package/src/shared-types.ts +4 -8
- package/src/uam-transaction.ts +68 -0
- 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 {
|
|
@@ -757,9 +622,16 @@ class RestoreWorkflow {
|
|
|
757
622
|
): Promise<RestorableResource | null> {
|
|
758
623
|
const resources = pkg.listResources() as RestorableResource[];
|
|
759
624
|
const existing = this._findResourceByFile(resources, owner, 'ImageResource', fileName);
|
|
760
|
-
if (existing) return existing;
|
|
761
625
|
const sourcePath = await this._resolveLooseSourceFile(pkg, sourceDir, fileName);
|
|
762
|
-
if (!sourcePath) return null;
|
|
626
|
+
if (!sourcePath) return existing ?? null;
|
|
627
|
+
if (existing) {
|
|
628
|
+
existing.setExtras?.({
|
|
629
|
+
...(existing.getExtras?.() ?? {}),
|
|
630
|
+
_publishedFile: fileBaseName(sourcePath),
|
|
631
|
+
_restoreAsLooseImage: true,
|
|
632
|
+
});
|
|
633
|
+
return existing;
|
|
634
|
+
}
|
|
763
635
|
const resource = doc.createImageResource(stripExtension(fileName));
|
|
764
636
|
resource
|
|
765
637
|
.setId(generateId())
|
|
@@ -772,7 +644,7 @@ class RestoreWorkflow {
|
|
|
772
644
|
...(resource.getExtras?.() ?? {}),
|
|
773
645
|
_publishedFile: fileBaseName(sourcePath),
|
|
774
646
|
_suppressPackageSize: true,
|
|
775
|
-
|
|
647
|
+
_restoreAsLooseImage: true,
|
|
776
648
|
});
|
|
777
649
|
pkg.addResource(resource);
|
|
778
650
|
return resource as RestorableResource;
|
|
@@ -1022,8 +894,8 @@ class RestoreWorkflow {
|
|
|
1022
894
|
warnings: string[],
|
|
1023
895
|
): Promise<void> {
|
|
1024
896
|
for (const resource of pkg.listResources() as RestorableResource[]) {
|
|
1025
|
-
const
|
|
1026
|
-
if (!['SoundResource', 'MiscResource', 'SpineResource', 'DragonBonesResource'].includes(resource.propertyType) && !
|
|
897
|
+
const restoreAsLooseImage = resource.getExtras?.()?._restoreAsLooseImage === true;
|
|
898
|
+
if (!['SoundResource', 'MiscResource', 'SpineResource', 'DragonBonesResource'].includes(resource.propertyType) && !restoreAsLooseImage) {
|
|
1027
899
|
continue;
|
|
1028
900
|
}
|
|
1029
901
|
const fileName = resourceFileName(resource);
|
|
@@ -1065,52 +937,7 @@ class RestoreWorkflow {
|
|
|
1065
937
|
|
|
1066
938
|
const outputPath = this._resourceOutputPath(outputProjectPath, pkg, resource, fileName);
|
|
1067
939
|
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
|
-
];
|
|
940
|
+
await this._fs.writeFile(outputPath, serializeFont(pkg, resource, glyphs));
|
|
1114
941
|
}
|
|
1115
942
|
|
|
1116
943
|
private async _writeMovieClipFile(
|
|
@@ -1157,7 +984,7 @@ class RestoreWorkflow {
|
|
|
1157
984
|
|
|
1158
985
|
const outputPath = this._resourceOutputPath(options.outputProjectPath, pkg, resource, fileName);
|
|
1159
986
|
await this._mkdirForFile(outputPath);
|
|
1160
|
-
await this._fs.writeFileRaw(outputPath,
|
|
987
|
+
await this._fs.writeFileRaw(outputPath, serializeMovieClip(resource, frames, textures));
|
|
1161
988
|
}
|
|
1162
989
|
|
|
1163
990
|
private async _writeSyntheticFontGlyphImages(pkg: Package, outputProjectPath: string): Promise<void> {
|
|
@@ -1187,55 +1014,19 @@ class RestoreWorkflow {
|
|
|
1187
1014
|
return sprites;
|
|
1188
1015
|
}
|
|
1189
1016
|
|
|
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
1017
|
private _sourceFileCandidates(pkg: Package, fileName: string, outputFileName = fileName): string[] {
|
|
1232
1018
|
const publishName = pkg.getPublishName() || pkg.getName();
|
|
1233
|
-
|
|
1019
|
+
assertSafeRestoreSegment(publishName, 'package publish name');
|
|
1020
|
+
assertSafeRestoreSegment(fileName, 'published source file name');
|
|
1021
|
+
assertSafeRestoreSegment(outputFileName, 'published source file name');
|
|
1022
|
+
const candidates = Array.from(new Set([
|
|
1234
1023
|
`${publishName}_${fileName}`,
|
|
1235
1024
|
fileName,
|
|
1236
1025
|
`${publishName}_${outputFileName}`,
|
|
1237
1026
|
outputFileName,
|
|
1238
1027
|
]));
|
|
1028
|
+
for (const candidate of candidates) assertSafeRestoreSegment(candidate, 'published source file name');
|
|
1029
|
+
return candidates;
|
|
1239
1030
|
}
|
|
1240
1031
|
|
|
1241
1032
|
private async _resolveLooseSourceFile(pkg: Package, sourceDir: string, outputFileName: string): Promise<string | null> {
|
|
@@ -1248,9 +1039,16 @@ class RestoreWorkflow {
|
|
|
1248
1039
|
}
|
|
1249
1040
|
|
|
1250
1041
|
private async _resolveSourceFile(sourceDir: string, candidates: string[]): Promise<string | null> {
|
|
1042
|
+
const resolvedSourceDir = await Promise.resolve(this._fs.resolvePath(sourceDir));
|
|
1251
1043
|
for (const candidate of candidates) {
|
|
1044
|
+
assertSafeRestoreSegment(candidate, 'published source file name');
|
|
1252
1045
|
const sourcePath = this._fs.join(sourceDir, candidate);
|
|
1253
|
-
if (await this._fs.isFile(sourcePath))
|
|
1046
|
+
if (!(await this._fs.isFile(sourcePath))) continue;
|
|
1047
|
+
const resolvedSourcePath = await Promise.resolve(this._fs.resolvePath(sourcePath));
|
|
1048
|
+
if (!isPathWithin(resolvedSourceDir, resolvedSourcePath)) {
|
|
1049
|
+
throw new Error(`restore: Published source file resolves outside the input directory: ${candidate}.`);
|
|
1050
|
+
}
|
|
1051
|
+
return resolvedSourcePath;
|
|
1254
1052
|
}
|
|
1255
1053
|
return null;
|
|
1256
1054
|
}
|
|
@@ -1263,6 +1061,9 @@ class RestoreWorkflow {
|
|
|
1263
1061
|
): string {
|
|
1264
1062
|
const basePath = this._fs.dirname(outputProjectPath);
|
|
1265
1063
|
const branch = resource.getBranch?.() ?? '';
|
|
1064
|
+
assertSafeRestoreSegment(pkg.getName(), 'package name');
|
|
1065
|
+
if (branch) assertSafeRestoreSegment(branch, 'branch name');
|
|
1066
|
+
assertSafeRestoreSegment(fileName, 'resource file name');
|
|
1266
1067
|
const assetsDir = branch ? `assets_${branch}` : 'assets';
|
|
1267
1068
|
const virtualPath = normalizeVirtualPath(resource.getPath?.());
|
|
1268
1069
|
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
|
@@ -20,8 +20,10 @@ export interface ApplyUamTransactionAppError {
|
|
|
20
20
|
opIndex?: number;
|
|
21
21
|
opId?: string;
|
|
22
22
|
opKind?: UamTransactionOperation['kind'];
|
|
23
|
+
operationKind?: UamTransactionOperation['kind'];
|
|
23
24
|
selector?: Record<string, unknown>;
|
|
24
25
|
issues?: UamValidationIssue[] | UamTransactionSupportIssue[];
|
|
26
|
+
diagnostics: ApplyUamTransactionAppDiagnostic[];
|
|
25
27
|
}
|
|
26
28
|
|
|
27
29
|
export type ApplyUamTransactionAppResult =
|
|
@@ -41,6 +43,70 @@ function mapTransactionErrorStage(error: UamTransactionError): ApplyUamTransacti
|
|
|
41
43
|
}
|
|
42
44
|
}
|
|
43
45
|
|
|
46
|
+
export interface ApplyUamTransactionAppDiagnostic {
|
|
47
|
+
code: string;
|
|
48
|
+
message: string;
|
|
49
|
+
severity: 'error';
|
|
50
|
+
path?: string;
|
|
51
|
+
nodeKind?: string;
|
|
52
|
+
resourceKind?: string;
|
|
53
|
+
gearKind?: string;
|
|
54
|
+
field?: string;
|
|
55
|
+
operationKind?: UamTransactionOperation['kind'];
|
|
56
|
+
opIndex?: number;
|
|
57
|
+
opId?: string;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function compactDiagnostic(diagnostic: ApplyUamTransactionAppDiagnostic): ApplyUamTransactionAppDiagnostic {
|
|
61
|
+
return Object.fromEntries(Object.entries(diagnostic).filter(([, value]) => value !== undefined)) as ApplyUamTransactionAppDiagnostic;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function isTransactionSupportIssue(issue: UamValidationIssue | UamTransactionSupportIssue): issue is UamTransactionSupportIssue {
|
|
65
|
+
return 'code' in issue;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function mapTransactionDiagnostics(error: UamTransactionError): ApplyUamTransactionAppDiagnostic[] {
|
|
69
|
+
if (error.issues?.length) {
|
|
70
|
+
return error.issues.map((issue) => {
|
|
71
|
+
if (isTransactionSupportIssue(issue)) {
|
|
72
|
+
return compactDiagnostic({
|
|
73
|
+
code: issue.code,
|
|
74
|
+
message: issue.message,
|
|
75
|
+
severity: 'error' as const,
|
|
76
|
+
path: issue.path,
|
|
77
|
+
nodeKind: issue.nodeKind,
|
|
78
|
+
resourceKind: issue.resourceKind,
|
|
79
|
+
gearKind: issue.gearKind,
|
|
80
|
+
field: issue.field,
|
|
81
|
+
operationKind: issue.operationKind ?? error.opKind,
|
|
82
|
+
opIndex: error.opIndex,
|
|
83
|
+
opId: error.opId,
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
return compactDiagnostic({
|
|
87
|
+
code: error.code,
|
|
88
|
+
message: issue.message,
|
|
89
|
+
severity: 'error' as const,
|
|
90
|
+
path: issue.path,
|
|
91
|
+
operationKind: error.opKind,
|
|
92
|
+
opIndex: error.opIndex,
|
|
93
|
+
opId: error.opId,
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
return [
|
|
98
|
+
compactDiagnostic({
|
|
99
|
+
code: error.code,
|
|
100
|
+
message: error.message,
|
|
101
|
+
severity: 'error',
|
|
102
|
+
path: error.opIndex === undefined ? undefined : `operations[${error.opIndex}]`,
|
|
103
|
+
operationKind: error.opKind,
|
|
104
|
+
opIndex: error.opIndex,
|
|
105
|
+
opId: error.opId,
|
|
106
|
+
}),
|
|
107
|
+
];
|
|
108
|
+
}
|
|
109
|
+
|
|
44
110
|
export function applyUamTransactionApp(input: ApplyUamTransactionAppInput): ApplyUamTransactionAppResult {
|
|
45
111
|
try {
|
|
46
112
|
return {
|
|
@@ -58,8 +124,10 @@ export function applyUamTransactionApp(input: ApplyUamTransactionAppInput): Appl
|
|
|
58
124
|
opIndex: error.opIndex,
|
|
59
125
|
opId: error.opId,
|
|
60
126
|
opKind: error.opKind,
|
|
127
|
+
operationKind: error.opKind,
|
|
61
128
|
selector: error.selector,
|
|
62
129
|
issues: error.issues,
|
|
130
|
+
diagnostics: mapTransactionDiagnostics(error),
|
|
63
131
|
},
|
|
64
132
|
};
|
|
65
133
|
}
|