@openfairygui/functions 0.2.0-alpha.30 → 0.2.0-alpha.32

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.
@@ -1,43 +1,18 @@
1
+ const require_publish = require("./publish-DCP0AYx2.cjs");
1
2
  let _openfairygui_core = require("@openfairygui/core");
2
3
  //#region src/restore-internals/output-transaction.ts
3
- function trimTrailingSlashes(value) {
4
- return value.replace(/[/\\]+$/, "");
5
- }
6
- function normalizeComparablePath(value) {
7
- const normalized = trimTrailingSlashes(value).replace(/\\/g, "/");
8
- const driveMatch = normalized.match(/^([a-z]:)(?:\/(.*))?$/i);
9
- const drivePrefix = driveMatch?.[1].toLowerCase() ?? "";
10
- const remainder = driveMatch ? driveMatch[2] ?? "" : normalized;
11
- const hasRoot = driveMatch ? true : remainder.startsWith("/");
12
- const rawSegments = remainder.split("/").filter((segment) => segment.length > 0);
13
- const segments = [];
14
- for (const segment of rawSegments) {
15
- if (segment === ".") continue;
16
- if (segment === "..") {
17
- if (segments.length > 0 && segments[segments.length - 1] !== "..") segments.pop();
18
- else if (!hasRoot) segments.push("..");
19
- continue;
20
- }
21
- segments.push(segment);
22
- }
23
- const joined = segments.join("/");
24
- return (drivePrefix ? `${drivePrefix}/${joined}`.replace(/\/$/, "") : hasRoot ? `/${joined}`.replace(/\/$/, "") : joined || ".").toLowerCase();
25
- }
26
4
  function isPathWithin(root, candidate) {
27
- const normalizedRoot = normalizeComparablePath(root);
28
- return normalizeComparablePath(candidate).startsWith(`${normalizedRoot}/`);
29
- }
30
- function basename(filePath) {
31
- return trimTrailingSlashes(filePath).match(/([^/\\]+)$/)?.[1] ?? "";
5
+ const normalizedRoot = require_publish.normalizeComparablePath(root);
6
+ return require_publish.normalizeComparablePath(candidate).startsWith(`${normalizedRoot}/`);
32
7
  }
33
8
  function normalizeRestoreOutputDir(output) {
34
- const normalized = trimTrailingSlashes(output);
35
- const name = basename(normalized);
9
+ const normalized = require_publish.trimTrailingSlashes(output);
10
+ const name = require_publish.basename(normalized);
36
11
  if (!normalized || /\.fairy$/i.test(normalized) || !name || name === "." || name === ".." || /^[a-z]:$/iu.test(name)) throw new Error("restore: Output must be a non-root project directory, not a .fairy file.");
37
12
  return normalized;
38
13
  }
39
14
  function resolveOutputProjectPath(outputDir, fs) {
40
- return fs.join(outputDir, `${basename(outputDir)}.fairy`);
15
+ return fs.join(outputDir, `${require_publish.basename(outputDir)}.fairy`);
41
16
  }
42
17
  async function resolvePathForContainment(filePath, fs) {
43
18
  const missingSegments = [];
@@ -45,7 +20,7 @@ async function resolvePathForContainment(filePath, fs) {
45
20
  while (!await fs.exists(existingPath)) {
46
21
  const parentPath = fs.dirname(existingPath);
47
22
  if (!parentPath || parentPath === existingPath) return Promise.resolve(fs.resolvePath(filePath));
48
- missingSegments.unshift(basename(existingPath));
23
+ missingSegments.unshift(require_publish.basename(existingPath));
49
24
  existingPath = parentPath;
50
25
  }
51
26
  const resolvedExistingPath = await Promise.resolve(fs.resolvePath(existingPath));
@@ -53,8 +28,8 @@ async function resolvePathForContainment(filePath, fs) {
53
28
  }
54
29
  async function assertRestoreOutputDir(inputDir, outputDir, fs, force) {
55
30
  const [resolvedInputDir, resolvedOutputDir] = await Promise.all([resolvePathForContainment(inputDir, fs), resolvePathForContainment(outputDir, fs)]);
56
- const normalizedInputDir = normalizeComparablePath(resolvedInputDir);
57
- const normalizedOutputDir = normalizeComparablePath(resolvedOutputDir);
31
+ const normalizedInputDir = require_publish.normalizeComparablePath(resolvedInputDir);
32
+ const normalizedOutputDir = require_publish.normalizeComparablePath(resolvedOutputDir);
58
33
  if (normalizedInputDir === normalizedOutputDir || isPathWithin(normalizedInputDir, normalizedOutputDir) || isPathWithin(normalizedOutputDir, normalizedInputDir)) throw new Error("Restore output directory must be independent from the published input directory.");
59
34
  if (!await fs.exists(outputDir)) return;
60
35
  let entries;
@@ -70,7 +45,7 @@ async function createRestoreStagingDir(outputDir, fs) {
70
45
  const parentDir = fs.dirname(outputDir) || ".";
71
46
  await fs.mkdir(parentDir);
72
47
  for (let attempt = 0; attempt < 8; attempt += 1) {
73
- const stagingDir = fs.join(parentDir, `.${basename(outputDir)}.restore-${(0, _openfairygui_core.generateId)()}`);
48
+ const stagingDir = fs.join(parentDir, `.${require_publish.basename(outputDir)}.restore-${(0, _openfairygui_core.generateId)()}`);
74
49
  if (await fs.exists(stagingDir)) continue;
75
50
  await fs.mkdir(stagingDir);
76
51
  return stagingDir;
@@ -85,7 +60,7 @@ async function commitRestoreOutput(stagingDir, outputDir, fs) {
85
60
  const parentDir = fs.dirname(outputDir) || ".";
86
61
  let backupDir = "";
87
62
  for (let attempt = 0; attempt < 8; attempt += 1) {
88
- const candidate = fs.join(parentDir, `.${basename(outputDir)}.restore-backup-${(0, _openfairygui_core.generateId)()}`);
63
+ const candidate = fs.join(parentDir, `.${require_publish.basename(outputDir)}.restore-backup-${(0, _openfairygui_core.generateId)()}`);
89
64
  if (!await fs.exists(candidate)) {
90
65
  backupDir = candidate;
91
66
  break;
@@ -413,7 +388,7 @@ function inferPackageName(fileName) {
413
388
  return fileName.replace(/\.bin$/i, "");
414
389
  }
415
390
  async function restore(options) {
416
- const sourceDir = trimTrailingSlashes(options.inputDir);
391
+ const sourceDir = require_publish.trimTrailingSlashes(options.inputDir);
417
392
  const outputDir = normalizeRestoreOutputDir(options.output);
418
393
  const outputProjectPath = resolveOutputProjectPath(outputDir, options.fs);
419
394
  await assertRestoreOutputDir(sourceDir, outputDir, options.fs, options.force === true);
@@ -433,7 +408,7 @@ async function restore(options) {
433
408
  extractImage: options.extractImage
434
409
  });
435
410
  const stagingDir = await createRestoreStagingDir(outputDir, options.fs);
436
- const stagingProjectPath = options.fs.join(stagingDir, basename(outputProjectPath));
411
+ const stagingProjectPath = options.fs.join(stagingDir, require_publish.basename(outputProjectPath));
437
412
  const warnings = [];
438
413
  try {
439
414
  await restorer.write(document, {
@@ -1,35 +1,10 @@
1
+ import { d as normalizeComparablePath, f as trimTrailingSlashes, u as basename } from "./publish-BJk8UzRP.js";
1
2
  import { BinaryReader, ProjectType, ProjectWriter, generateId } from "@openfairygui/core";
2
3
  //#region src/restore-internals/output-transaction.ts
3
- function trimTrailingSlashes(value) {
4
- return value.replace(/[/\\]+$/, "");
5
- }
6
- function normalizeComparablePath(value) {
7
- const normalized = trimTrailingSlashes(value).replace(/\\/g, "/");
8
- const driveMatch = normalized.match(/^([a-z]:)(?:\/(.*))?$/i);
9
- const drivePrefix = driveMatch?.[1].toLowerCase() ?? "";
10
- const remainder = driveMatch ? driveMatch[2] ?? "" : normalized;
11
- const hasRoot = driveMatch ? true : remainder.startsWith("/");
12
- const rawSegments = remainder.split("/").filter((segment) => segment.length > 0);
13
- const segments = [];
14
- for (const segment of rawSegments) {
15
- if (segment === ".") continue;
16
- if (segment === "..") {
17
- if (segments.length > 0 && segments[segments.length - 1] !== "..") segments.pop();
18
- else if (!hasRoot) segments.push("..");
19
- continue;
20
- }
21
- segments.push(segment);
22
- }
23
- const joined = segments.join("/");
24
- return (drivePrefix ? `${drivePrefix}/${joined}`.replace(/\/$/, "") : hasRoot ? `/${joined}`.replace(/\/$/, "") : joined || ".").toLowerCase();
25
- }
26
4
  function isPathWithin(root, candidate) {
27
5
  const normalizedRoot = normalizeComparablePath(root);
28
6
  return normalizeComparablePath(candidate).startsWith(`${normalizedRoot}/`);
29
7
  }
30
- function basename(filePath) {
31
- return trimTrailingSlashes(filePath).match(/([^/\\]+)$/)?.[1] ?? "";
32
- }
33
8
  function normalizeRestoreOutputDir(output) {
34
9
  const normalized = trimTrailingSlashes(output);
35
10
  const name = basename(normalized);
package/dist/web.cjs CHANGED
@@ -1,5 +1,5 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_publish = require("./publish-xFWT9Slz.cjs");
2
+ const require_publish = require("./publish-DCP0AYx2.cjs");
3
3
  let _openfairygui_core = require("@openfairygui/core");
4
4
  //#region src/adapters/web/raster.ts
5
5
  function getBrowserContext(canvas) {
package/dist/web.js CHANGED
@@ -1,4 +1,4 @@
1
- import { t as publish } from "./publish-BJ_eelME.js";
1
+ import { t as publish } from "./publish-BJk8UzRP.js";
2
2
  import { ProjectType } from "@openfairygui/core";
3
3
  //#region src/adapters/web/raster.ts
4
4
  function getBrowserContext(canvas) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openfairygui/functions",
3
- "version": "0.2.0-alpha.30",
3
+ "version": "0.2.0-alpha.32",
4
4
  "description": "FairyGUI Headless Authoring SDK — composable transform functions.",
5
5
  "author": "OpenFairyGUI Contributors",
6
6
  "license": "MIT",
@@ -74,7 +74,7 @@
74
74
  ],
75
75
  "dependencies": {
76
76
  "jiti": "^2.6.1",
77
- "@openfairygui/core": "0.2.0-alpha.30"
77
+ "@openfairygui/core": "0.2.0-alpha.32"
78
78
  },
79
79
  "optionalDependencies": {
80
80
  "sharp": ">=0.33.0"
@@ -1,13 +1,10 @@
1
1
  import type {
2
- Component,
3
2
  Document,
4
- DragonBonesResource,
5
3
  FontResource,
6
4
  ILogger,
7
5
  ImageResource,
8
6
  MovieClipResource,
9
7
  Package,
10
- SpineResource,
11
8
  } from '@openfairygui/core';
12
9
  import type { AtlasOptions } from '../atlas.js';
13
10
  import type {
@@ -15,6 +12,13 @@ import type {
15
12
  AtlasRasterInput,
16
13
  AtlasRasterResolvedBuffer,
17
14
  } from '../publish/contracts.js';
15
+ import {
16
+ isFontResource,
17
+ isImageResource,
18
+ isMovieClipResource,
19
+ resolveImageFileName,
20
+ resolveImagePath,
21
+ } from '../publish/package-context.js';
18
22
  import type { ExtrasMap } from '../shared-types.js';
19
23
  import { parseFnt } from './font.js';
20
24
  import { extractJtaFrames } from './jta.js';
@@ -46,7 +50,6 @@ export function getPublishedItemId(resource: { getId(): string; getExtras(): Ext
46
50
  }
47
51
 
48
52
  interface ImageResourceExtras extends ExtrasMap {
49
- _fileName?: string;
50
53
  _publishedId?: string;
51
54
  }
52
55
 
@@ -63,11 +66,6 @@ export function resolveFontFileName(fontName: string): string {
63
66
  return /\.fnt$/i.test(fontName) ? fontName : `${fontName}.fnt`;
64
67
  }
65
68
 
66
- export function resolveImageFileName(resource: ImageResource): string {
67
- const extras = resource.getExtras() as ImageResourceExtras;
68
- return resource.getFileName() || extras._fileName || resource.getName();
69
- }
70
-
71
69
  /**
72
70
  * Trim transparent edges from an image using the host raster backend.
73
71
  * Returns the trimmed buffer, dimensions, and offsets.
@@ -154,19 +152,6 @@ async function _trimImage(
154
152
  /**
155
153
  * Resolve an ImageResource to its actual file path on disk.
156
154
  */
157
- export function resolveImagePath(resource: ImageResource, pkg: Package, basePath: string): string {
158
- const imgPath = resource.getPath() ?? '/';
159
- const fileName = resolveImageFileName(resource);
160
- const branchName = resource.getBranch?.() ?? '';
161
- const normalizedBasePath = basePath.replace(/[/\\]+$/, '');
162
- const packageBasePath = !branchName
163
- ? normalizedBasePath
164
- : /[\\/]assets$/i.test(normalizedBasePath)
165
- ? normalizedBasePath.replace(/([\\/])assets$/i, `$1assets_${branchName}`)
166
- : `${normalizedBasePath}_${branchName}`;
167
- return `${packageBasePath}/${pkg.getName()}${imgPath}${fileName}`;
168
- }
169
-
170
155
  export type InputItem = {
171
156
  id: string;
172
157
  width: number;
@@ -486,26 +471,6 @@ export async function collectFontTexture(
486
471
  }
487
472
  }
488
473
 
489
- export function isComponentResource(resource: PackageResource): resource is Component {
490
- return resource.propertyType === 'Component';
491
- }
492
-
493
- export function isImageResource(resource: PackageResource): resource is ImageResource {
494
- return resource.propertyType === 'ImageResource';
495
- }
496
-
497
- export function isMovieClipResource(resource: PackageResource): resource is MovieClipResource {
498
- return resource.propertyType === 'MovieClipResource';
499
- }
500
-
501
- export function isSkeletonResource(resource: PackageResource): resource is SpineResource | DragonBonesResource {
502
- return resource.propertyType === 'SpineResource' || resource.propertyType === 'DragonBonesResource';
503
- }
504
-
505
- export function isFontResource(resource: PackageResource): resource is FontResource {
506
- return resource.propertyType === 'FontResource';
507
- }
508
-
509
474
  export function isPackableResource(resource: PackageResource): resource is PackableResource {
510
475
  return isImageResource(resource) || isMovieClipResource(resource) || isFontResource(resource);
511
476
  }
@@ -3,13 +3,16 @@ import type { AtlasOptions } from '../atlas.js';
3
3
  import { COMPAT_NODE_RECT_FLAGS, type CompatNodeRect } from '../max-rects-compat.js';
4
4
  import { MaxRectsPackerCompat } from '../max-rects-packer-compat.js';
5
5
  import type { AtlasRasterBackend } from '../publish/contracts.js';
6
- import { parseTextureSetMode, type TextureSetMode } from '../utils.js';
7
6
  import {
8
- getPublishedItemId,
7
+ extname,
9
8
  isFontResource,
10
9
  isImageResource,
11
10
  resolveImageFileName,
12
11
  resolveImagePath,
12
+ } from '../publish/package-context.js';
13
+ import { parseTextureSetMode, type TextureSetMode } from '../utils.js';
14
+ import {
15
+ getPublishedItemId,
13
16
  type FontResourceExtras,
14
17
  type InputItem,
15
18
  type PackageResource,
@@ -612,14 +615,6 @@ function resolveStandaloneAtlasSize(
612
615
  return resolveDirectOutputAtlasSize(width, height, options);
613
616
  }
614
617
 
615
- function extname(fileName: string): string {
616
- const normalized = fileName.replace(/\\/g, '/');
617
- const lastSlash = normalized.lastIndexOf('/');
618
- const lastDot = normalized.lastIndexOf('.');
619
- if (lastDot <= lastSlash) return '';
620
- return normalized.slice(lastDot);
621
- }
622
-
623
618
  function insertFileNameSuffix(fileName: string, suffix: string): string {
624
619
  const extension = extname(fileName);
625
620
  if (!extension) return `${fileName}${suffix}`;
package/src/atlas.ts CHANGED
@@ -7,6 +7,13 @@ import {
7
7
  TransitionActionType,
8
8
  } from '@openfairygui/core';
9
9
  import type { AtlasRasterBackend } from './publish/contracts.js';
10
+ import {
11
+ isComponentResource,
12
+ isFontResource,
13
+ isImageResource,
14
+ isMovieClipResource,
15
+ isSkeletonResource,
16
+ } from './publish/package-context.js';
10
17
  import { collectPackageResourceReferences } from './publish/resource-references.js';
11
18
  import type { ExtrasMap, HasOptionalSrc, HasOptionalUrl } from './shared-types.js';
12
19
  import { createTransform } from './utils.js';
@@ -14,12 +21,7 @@ import {
14
21
  collectFontTexture,
15
22
  collectImage,
16
23
  collectMovieClipFrames,
17
- isComponentResource,
18
- isFontResource,
19
- isImageResource,
20
- isMovieClipResource,
21
24
  isPackableResource,
22
- isSkeletonResource,
23
25
  resolveFontFileName,
24
26
  type InputItem,
25
27
  type PackageResource,
package/src/codegen.ts CHANGED
@@ -13,6 +13,7 @@ import {
13
13
  UNITY_COMPONENT_TEMPLATE,
14
14
  } from './codegen-templates.js';
15
15
  import { formatPluginError, type LoadedPlugin } from './plugins/types.js';
16
+ import { dirname, isAbsolutePathLike, trimTrailingSlashes } from './path-utils.js';
16
17
  import type { PublishFileSystem } from './publish/contracts.js';
17
18
  import type { CliCodeGenerationSettings, RootProjectSettings } from './shared-types.js';
18
19
 
@@ -583,7 +584,7 @@ function resolveCodePath(
583
584
  basePath: string | undefined,
584
585
  fs: Pick<PublishFileSystem, 'join'>,
585
586
  ): string {
586
- if (isAbsolutePath(codePath)) return trimTrailingSlashes(codePath);
587
+ if (isAbsolutePathLike(codePath)) return trimTrailingSlashes(codePath);
587
588
  const projectBasePath = resolveProjectBasePath(basePath);
588
589
  return projectBasePath ? trimTrailingSlashes(fs.join(projectBasePath, codePath)) : trimTrailingSlashes(codePath);
589
590
  }
@@ -596,20 +597,6 @@ export function resolveProjectBasePath(basePath: string | undefined): string {
596
597
  return dirname(normalized);
597
598
  }
598
599
 
599
- function dirname(filePath: string): string {
600
- const trimmed = trimTrailingSlashes(filePath);
601
- const match = trimmed.match(/^(.*)[/\\][^/\\]+$/);
602
- return match?.[1] ?? '';
603
- }
604
-
605
- function trimTrailingSlashes(value: string): string {
606
- return value.replace(/[/\\]+$/, '');
607
- }
608
-
609
- function isAbsolutePath(value: string): boolean {
610
- return /^[a-z]:[/\\]/i.test(value) || value.startsWith('/') || value.startsWith('\\\\');
611
- }
612
-
613
600
  function isDefaultMemberName(ownerType: string, kind: CodegenMember['kind'], name: string): boolean {
614
601
  if (kind === 'controller') {
615
602
  return (ownerType === 'GButton' || ownerType === 'GComboBox') && name === 'button';
@@ -0,0 +1,40 @@
1
+ export function trimTrailingSlashes(value: string): string {
2
+ return value.replace(/[/\\]+$/, '');
3
+ }
4
+
5
+ export function dirname(filePath: string): string {
6
+ const match = trimTrailingSlashes(filePath).match(/^(.*)[/\\][^/\\]+$/);
7
+ return match?.[1] ?? '';
8
+ }
9
+
10
+ export function basename(filePath: string): string {
11
+ const match = trimTrailingSlashes(filePath).match(/([^/\\]+)$/);
12
+ return match?.[1] ?? '';
13
+ }
14
+
15
+ export function isAbsolutePathLike(value: string): boolean {
16
+ return /^(?:[a-zA-Z]:[/\\]|[/\\]{1,2})/u.test(value);
17
+ }
18
+
19
+ export function normalizeComparablePath(value: string): string {
20
+ const normalized = trimTrailingSlashes(value).replace(/\\/g, '/');
21
+ const driveMatch = normalized.match(/^([a-z]:)(?:\/(.*))?$/i);
22
+ const drivePrefix = driveMatch?.[1].toLowerCase() ?? '';
23
+ const remainder = driveMatch ? (driveMatch[2] ?? '') : normalized;
24
+ const hasRoot = driveMatch ? true : remainder.startsWith('/');
25
+ const segments: string[] = [];
26
+
27
+ for (const segment of remainder.split('/').filter(Boolean)) {
28
+ if (segment === '.') continue;
29
+ if (segment === '..') {
30
+ if (segments.length > 0 && segments.at(-1) !== '..') segments.pop();
31
+ else if (!hasRoot) segments.push('..');
32
+ continue;
33
+ }
34
+ segments.push(segment);
35
+ }
36
+
37
+ const joined = segments.join('/');
38
+ const comparable = drivePrefix ? `${drivePrefix}/${joined}` : hasRoot ? `/${joined}` : joined || '.';
39
+ return comparable.replace(/\/$/, '').toLowerCase();
40
+ }
@@ -39,7 +39,7 @@ interface PackagePublishContext {
39
39
 
40
40
  const UNITY_PROJECT_TYPE = ProjectType.Unity;
41
41
 
42
- function isComponentResource(resource: ReturnType<Package['listResources']>[number]): resource is Component {
42
+ export function isComponentResource(resource: ReturnType<Package['listResources']>[number]): resource is Component {
43
43
  return resource.propertyType === 'Component';
44
44
  }
45
45
 
@@ -47,7 +47,7 @@ export function isImageResource(resource: ReturnType<Package['listResources']>[n
47
47
  return resource.propertyType === 'ImageResource';
48
48
  }
49
49
 
50
- function isMovieClipResource(resource: ReturnType<Package['listResources']>[number]): resource is MovieClipResource {
50
+ export function isMovieClipResource(resource: ReturnType<Package['listResources']>[number]): resource is MovieClipResource {
51
51
  return resource.propertyType === 'MovieClipResource';
52
52
  }
53
53
 
@@ -61,7 +61,7 @@ export function isMiscResource(resource: ReturnType<Package['listResources']>[nu
61
61
  return resource.propertyType === 'MiscResource';
62
62
  }
63
63
 
64
- function isFontResource(resource: ReturnType<Package['listResources']>[number]): resource is FontResource {
64
+ export function isFontResource(resource: ReturnType<Package['listResources']>[number]): resource is FontResource {
65
65
  return resource.propertyType === 'FontResource';
66
66
  }
67
67
 
package/src/publish.ts CHANGED
@@ -8,6 +8,7 @@ import {
8
8
  } from '@openfairygui/core';
9
9
  import { atlas } from './atlas.js';
10
10
  import { publishCodeGeneration, resolveProjectBasePath } from './codegen.js';
11
+ import { dirname, isAbsolutePathLike, trimTrailingSlashes } from './path-utils.js';
11
12
  import { formatPluginError, type LoadedPlugin } from './plugins/types.js';
12
13
  import type { PublishFileSystem } from './publish/contracts.js';
13
14
  import {
@@ -73,14 +74,6 @@ async function runPublishPluginHook(
73
74
  }
74
75
  }
75
76
 
76
- function trimTrailingSlashes(value: string): string {
77
- return value.replace(/[/\\]+$/, '');
78
- }
79
-
80
- function isAbsolutePathLike(value: string): boolean {
81
- return /^(?:[a-zA-Z]:[/\\]|[/\\]{1,2})/u.test(value);
82
- }
83
-
84
77
  function joinPathSegments(left: string, right: string): string {
85
78
  const normalizedLeft = trimTrailingSlashes(left);
86
79
  const normalizedRight = right.replace(/^[/\\]+/, '');
@@ -90,12 +83,6 @@ function joinPathSegments(left: string, right: string): string {
90
83
  return `${normalizedLeft}${separator}${normalizedRight}`;
91
84
  }
92
85
 
93
- function dirname(filePath: string): string {
94
- const trimmed = filePath.replace(/[/\\]+$/, '');
95
- const match = trimmed.match(/^(.*)[/\\][^/\\]+$/);
96
- return match?.[1] ?? '';
97
- }
98
-
99
86
  function createUnsupportedFsOperation(name: keyof FileSystem) {
100
87
  return async (): Promise<never> => {
101
88
  throw new Error(`publish: FileSystem.${name}() is not available in the publish writer adapter.`);
@@ -1,42 +1,8 @@
1
1
  import { generateId } from '@openfairygui/core';
2
+ import { basename, normalizeComparablePath, trimTrailingSlashes } from '../path-utils.js';
2
3
  import type { RestoreFileSystem } from '../restore.js';
3
4
 
4
- export function trimTrailingSlashes(value: string): string {
5
- return value.replace(/[/\\]+$/, '');
6
- }
7
-
8
- function normalizeComparablePath(value: string): string {
9
- const normalized = trimTrailingSlashes(value).replace(/\\/g, '/');
10
- const driveMatch = normalized.match(/^([a-z]:)(?:\/(.*))?$/i);
11
- const drivePrefix = driveMatch?.[1].toLowerCase() ?? '';
12
- const remainder = driveMatch ? (driveMatch[2] ?? '') : normalized;
13
- const hasRoot = driveMatch ? true : remainder.startsWith('/');
14
- const rawSegments = remainder.split('/').filter((segment) => segment.length > 0);
15
- const segments: string[] = [];
16
-
17
- for (const segment of rawSegments) {
18
- if (segment === '.') continue;
19
- if (segment === '..') {
20
- if (segments.length > 0 && segments[segments.length - 1] !== '..') {
21
- segments.pop();
22
- } else if (!hasRoot) {
23
- segments.push('..');
24
- }
25
- continue;
26
- }
27
- segments.push(segment);
28
- }
29
-
30
- const joined = segments.join('/');
31
- const comparable = drivePrefix
32
- ? `${drivePrefix}/${joined}`.replace(/\/$/, '')
33
- : hasRoot
34
- ? `/${joined}`.replace(/\/$/, '')
35
- : joined || '.';
36
- // Restore prefers a conservative same-directory guard: false positives are safer than
37
- // missing a Windows-style case-only path alias and deleting the source publish dir.
38
- return comparable.toLowerCase();
39
- }
5
+ export { basename, trimTrailingSlashes } from '../path-utils.js';
40
6
 
41
7
  export function isPathWithin(root: string, candidate: string): boolean {
42
8
  const normalizedRoot = normalizeComparablePath(root);
@@ -44,12 +10,6 @@ export function isPathWithin(root: string, candidate: string): boolean {
44
10
  return normalizedCandidate.startsWith(`${normalizedRoot}/`);
45
11
  }
46
12
 
47
- export function basename(filePath: string): string {
48
- const trimmed = trimTrailingSlashes(filePath);
49
- const match = trimmed.match(/([^/\\]+)$/);
50
- return match?.[1] ?? '';
51
- }
52
-
53
13
  export function normalizeRestoreOutputDir(output: string): string {
54
14
  const normalized = trimTrailingSlashes(output);
55
15
  const name = basename(normalized);