@loomhelix/deptrail 0.1.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/LICENSE +21 -0
- package/README.md +421 -0
- package/dist/actions/action-error.js +11 -0
- package/dist/actions/action-jobs.js +199 -0
- package/dist/actions/action-service.js +268 -0
- package/dist/actions/apply.js +120 -0
- package/dist/actions/availability.js +79 -0
- package/dist/actions/change-sets.js +117 -0
- package/dist/actions/dry-run.js +287 -0
- package/dist/actions/edge-moves.js +108 -0
- package/dist/actions/edit-plan.js +179 -0
- package/dist/actions/file-scope.js +118 -0
- package/dist/actions/fix-options.js +141 -0
- package/dist/actions/goal-check.js +80 -0
- package/dist/actions/install.js +27 -0
- package/dist/actions/job-registry.js +104 -0
- package/dist/actions/json-edit.js +189 -0
- package/dist/actions/labels.js +35 -0
- package/dist/actions/node-actions.js +102 -0
- package/dist/actions/overrides-location.js +59 -0
- package/dist/actions/parent-chain.js +210 -0
- package/dist/actions/range-style.js +38 -0
- package/dist/actions/recipes.js +31 -0
- package/dist/actions/root-files.js +30 -0
- package/dist/actions/sandbox.js +175 -0
- package/dist/actions/set-range.js +59 -0
- package/dist/actions/shared-packages.js +50 -0
- package/dist/actions/text-diff.js +203 -0
- package/dist/actions/types.js +1 -0
- package/dist/actions/update-options.js +232 -0
- package/dist/actions/workspace-edit.js +33 -0
- package/dist/adapters/command-runner.js +127 -0
- package/dist/adapters/manifest-only.js +20 -0
- package/dist/adapters/node-modules.js +148 -0
- package/dist/adapters/npm-lock.js +149 -0
- package/dist/adapters/package-manifest.js +57 -0
- package/dist/adapters/pnpm-lock.js +177 -0
- package/dist/adapters/resolve-today.js +93 -0
- package/dist/cli/cli.js +227 -0
- package/dist/cli/open-browser.js +14 -0
- package/dist/cli/package-version.js +6 -0
- package/dist/cli/usage-text.js +27 -0
- package/dist/config/glob.js +119 -0
- package/dist/config/npmrc.js +116 -0
- package/dist/config/root-config.js +103 -0
- package/dist/config/secrets.js +18 -0
- package/dist/discovery/find-roots.js +107 -0
- package/dist/enrich/credentials.js +143 -0
- package/dist/enrich/enrichment-cache.js +42 -0
- package/dist/enrich/enrichment-keys.js +71 -0
- package/dist/enrich/enrichment-service.js +399 -0
- package/dist/enrich/enrichment-status.js +86 -0
- package/dist/enrich/enrichment-store.js +143 -0
- package/dist/enrich/http.js +201 -0
- package/dist/enrich/on-demand.js +220 -0
- package/dist/enrich/osv-client.js +260 -0
- package/dist/enrich/registry-client.js +123 -0
- package/dist/enrich/types.js +1 -0
- package/dist/git/blob-reader.js +180 -0
- package/dist/git/git-commands.js +32 -0
- package/dist/git/git-process.js +71 -0
- package/dist/git/git-repository.js +136 -0
- package/dist/git/ref-list.js +78 -0
- package/dist/git/tree-index.js +87 -0
- package/dist/graph/global-index.js +19 -0
- package/dist/graph/root-graph-builder.js +114 -0
- package/dist/graph/serialize.js +19 -0
- package/dist/model.js +2 -0
- package/dist/query/children.js +38 -0
- package/dist/query/compare-shape.js +13 -0
- package/dist/query/compare.js +245 -0
- package/dist/query/decorations.js +121 -0
- package/dist/query/dependents.js +135 -0
- package/dist/query/duplicates.js +23 -0
- package/dist/query/enrichment-lookup.js +78 -0
- package/dist/query/fixes.js +60 -0
- package/dist/query/graph-diff.js +42 -0
- package/dist/query/issues.js +235 -0
- package/dist/query/lookup.js +49 -0
- package/dist/query/node-fix.js +46 -0
- package/dist/query/package-insight.js +112 -0
- package/dist/query/search.js +61 -0
- package/dist/query/subgraph.js +107 -0
- package/dist/query/versions.js +103 -0
- package/dist/scan/build-root.js +137 -0
- package/dist/scan/cache-shape.js +52 -0
- package/dist/scan/scan-cache.js +32 -0
- package/dist/scan/scan-session.js +127 -0
- package/dist/scan/source-registry.js +55 -0
- package/dist/server/auth.js +45 -0
- package/dist/server/branches.js +105 -0
- package/dist/server/error-log.js +9 -0
- package/dist/server/root-segment.js +13 -0
- package/dist/server/router.js +474 -0
- package/dist/server/server.js +166 -0
- package/dist/server/source-key.js +1 -0
- package/dist/server/static.js +59 -0
- package/dist/sources/file-reader.js +55 -0
- package/dist/sources/git-reader.js +45 -0
- package/dist/ui/assets/index-C4uyB5v3.css +1 -0
- package/dist/ui/assets/index-DYAG2LM-.js +51 -0
- package/dist/ui/index.html +15 -0
- package/package.json +72 -0
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { readRootConfig } from "../config/root-config.js";
|
|
2
|
+
import { findRoots } from "../discovery/find-roots.js";
|
|
3
|
+
import { buildGlobalIndex } from "../graph/global-index.js";
|
|
4
|
+
import { buildRoot, failedRootGraph, messageOf } from "./build-root.js";
|
|
5
|
+
import { cacheKeyOf } from "./scan-cache.js";
|
|
6
|
+
function scanningPlaceholder(root) {
|
|
7
|
+
return {
|
|
8
|
+
id: root.id,
|
|
9
|
+
path: root.path,
|
|
10
|
+
manager: root.manager,
|
|
11
|
+
source: root.lockfile ? 'lockfile' : 'manifest-only',
|
|
12
|
+
status: 'scanning',
|
|
13
|
+
importers: [],
|
|
14
|
+
packages: new Map(),
|
|
15
|
+
warnings: [],
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
export async function startScanSession(options) {
|
|
19
|
+
const { repoPath, reader, runner, cache, env, userNpmrc, onRootPublished } = options;
|
|
20
|
+
const allowInstalled = options.useInstalled ?? true;
|
|
21
|
+
let run = await discover();
|
|
22
|
+
let index = new Map();
|
|
23
|
+
let ready = Promise.resolve();
|
|
24
|
+
async function discover() {
|
|
25
|
+
const descriptors = await findRoots(reader);
|
|
26
|
+
const scan = {
|
|
27
|
+
repoPath,
|
|
28
|
+
roots: descriptors.map(scanningPlaceholder),
|
|
29
|
+
scannedAt: new Date().toISOString(),
|
|
30
|
+
...(options.skippedSubmodules === undefined ? {} : { skippedSubmodules: [...options.skippedSubmodules] }),
|
|
31
|
+
};
|
|
32
|
+
return { scan, descriptors, generations: new Map() };
|
|
33
|
+
}
|
|
34
|
+
async function buildWithCache(descriptor) {
|
|
35
|
+
const lockfileText = descriptor.lockfile && cache ? await reader.readText(descriptor.lockfile) : null;
|
|
36
|
+
const key = lockfileText === null ? null : cacheKeyOf(repoPath, descriptor.path, lockfileText);
|
|
37
|
+
const cached = key && cache ? await cache.get(key) : null;
|
|
38
|
+
if (cached)
|
|
39
|
+
return { ...cached, id: descriptor.id, path: descriptor.path, manager: descriptor.manager };
|
|
40
|
+
const graph = await buildRoot(descriptor, { reader, runner, allowResolve: false, allowInstalled });
|
|
41
|
+
if (key && cache && graph.source === 'lockfile' && graph.status === 'ready')
|
|
42
|
+
await cache.set(key, graph).catch(() => { });
|
|
43
|
+
return graph;
|
|
44
|
+
}
|
|
45
|
+
async function withConfig(descriptor, graph) {
|
|
46
|
+
if (graph.status === 'failed')
|
|
47
|
+
return graph;
|
|
48
|
+
graph.config = await readRootConfig(reader, descriptor.path, { userNpmrc, env, expandProjectVariables: options.expandNpmrcVariables ?? true });
|
|
49
|
+
graph.warnings = [...graph.warnings, ...graph.config.warnings];
|
|
50
|
+
return graph;
|
|
51
|
+
}
|
|
52
|
+
function descriptorOf(target, rootId) {
|
|
53
|
+
const descriptor = target.descriptors.find((candidate) => candidate.id === rootId);
|
|
54
|
+
if (!descriptor)
|
|
55
|
+
throw new Error(`Unknown root ${rootId}`);
|
|
56
|
+
return descriptor;
|
|
57
|
+
}
|
|
58
|
+
function beginBuild(target, descriptor) {
|
|
59
|
+
const generation = (target.generations.get(descriptor.id) ?? 0) + 1;
|
|
60
|
+
target.generations.set(descriptor.id, generation);
|
|
61
|
+
return generation;
|
|
62
|
+
}
|
|
63
|
+
function publish(target, descriptor, generation, graph) {
|
|
64
|
+
if (target !== run || target.generations.get(descriptor.id) !== generation)
|
|
65
|
+
return;
|
|
66
|
+
const position = target.scan.roots.findIndex((candidate) => candidate.id === descriptor.id);
|
|
67
|
+
if (position === -1)
|
|
68
|
+
return;
|
|
69
|
+
target.scan.roots[position] = graph;
|
|
70
|
+
index = buildGlobalIndex(target.scan.roots);
|
|
71
|
+
onRootPublished?.(target.scan.roots);
|
|
72
|
+
}
|
|
73
|
+
async function runBuild(target, descriptor, generation, build) {
|
|
74
|
+
let graph;
|
|
75
|
+
try {
|
|
76
|
+
graph = await withConfig(descriptor, await build());
|
|
77
|
+
}
|
|
78
|
+
catch (failure) {
|
|
79
|
+
graph = failedRootGraph(descriptor, messageOf(failure));
|
|
80
|
+
}
|
|
81
|
+
publish(target, descriptor, generation, graph);
|
|
82
|
+
}
|
|
83
|
+
function buildAll(target) {
|
|
84
|
+
const builds = target.descriptors.map((descriptor) => {
|
|
85
|
+
const generation = beginBuild(target, descriptor);
|
|
86
|
+
return runBuild(target, descriptor, generation, () => buildWithCache(descriptor));
|
|
87
|
+
});
|
|
88
|
+
return Promise.all(builds).then(() => undefined);
|
|
89
|
+
}
|
|
90
|
+
async function startRun() {
|
|
91
|
+
index = new Map();
|
|
92
|
+
ready = buildAll(run);
|
|
93
|
+
await ready;
|
|
94
|
+
}
|
|
95
|
+
ready = buildAll(run);
|
|
96
|
+
return {
|
|
97
|
+
get scan() {
|
|
98
|
+
return run.scan;
|
|
99
|
+
},
|
|
100
|
+
get index() {
|
|
101
|
+
return index;
|
|
102
|
+
},
|
|
103
|
+
get ready() {
|
|
104
|
+
return ready;
|
|
105
|
+
},
|
|
106
|
+
root(id) {
|
|
107
|
+
return run.scan.roots.find((candidate) => candidate.id === id);
|
|
108
|
+
},
|
|
109
|
+
async resolve(rootId) {
|
|
110
|
+
const target = run;
|
|
111
|
+
const descriptor = descriptorOf(target, rootId);
|
|
112
|
+
const generation = beginBuild(target, descriptor);
|
|
113
|
+
publish(target, descriptor, generation, scanningPlaceholder(descriptor));
|
|
114
|
+
await runBuild(target, descriptor, generation, () => buildRoot(descriptor, { reader, runner, allowResolve: true, allowInstalled }));
|
|
115
|
+
},
|
|
116
|
+
async refreshRoot(rootId) {
|
|
117
|
+
const target = run;
|
|
118
|
+
const descriptor = descriptorOf(target, rootId);
|
|
119
|
+
const generation = beginBuild(target, descriptor);
|
|
120
|
+
await runBuild(target, descriptor, generation, () => buildWithCache(descriptor));
|
|
121
|
+
},
|
|
122
|
+
async rescan() {
|
|
123
|
+
run = await discover();
|
|
124
|
+
await startRun();
|
|
125
|
+
},
|
|
126
|
+
};
|
|
127
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
export const COMMIT_SOURCE_LIMIT = 4;
|
|
2
|
+
export function createSourceRegistry(options) {
|
|
3
|
+
const { working } = options;
|
|
4
|
+
const limit = options.limit ?? COMMIT_SOURCE_LIMIT;
|
|
5
|
+
const open = new Map();
|
|
6
|
+
const opening = new Map();
|
|
7
|
+
function markUsed(source) {
|
|
8
|
+
open.delete(source.commit);
|
|
9
|
+
open.set(source.commit, source);
|
|
10
|
+
return source;
|
|
11
|
+
}
|
|
12
|
+
function allRoots() {
|
|
13
|
+
return [...working.scan.roots, ...[...open.values()].flatMap((source) => source.session.scan.roots)];
|
|
14
|
+
}
|
|
15
|
+
function rootsChanged() {
|
|
16
|
+
options.onRootsChanged(allRoots());
|
|
17
|
+
}
|
|
18
|
+
function dropBeyondLimit() {
|
|
19
|
+
for (const commit of open.keys()) {
|
|
20
|
+
if (open.size <= limit)
|
|
21
|
+
return;
|
|
22
|
+
open.delete(commit);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
async function startOpening(commit) {
|
|
26
|
+
const source = markUsed({ commit, session: await options.openSession(commit) });
|
|
27
|
+
dropBeyondLimit();
|
|
28
|
+
rootsChanged();
|
|
29
|
+
return source;
|
|
30
|
+
}
|
|
31
|
+
return {
|
|
32
|
+
working,
|
|
33
|
+
openCommit(commit) {
|
|
34
|
+
const known = open.get(commit);
|
|
35
|
+
if (known !== undefined)
|
|
36
|
+
return Promise.resolve(markUsed(known));
|
|
37
|
+
let pending = opening.get(commit);
|
|
38
|
+
if (pending === undefined) {
|
|
39
|
+
pending = startOpening(commit).finally(() => opening.delete(commit));
|
|
40
|
+
opening.set(commit, pending);
|
|
41
|
+
}
|
|
42
|
+
return pending;
|
|
43
|
+
},
|
|
44
|
+
commit(commit) {
|
|
45
|
+
const known = open.get(commit);
|
|
46
|
+
return known === undefined ? undefined : markUsed(known);
|
|
47
|
+
},
|
|
48
|
+
allRoots,
|
|
49
|
+
rootsChanged,
|
|
50
|
+
close() {
|
|
51
|
+
open.clear();
|
|
52
|
+
opening.clear();
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { randomBytes, timingSafeEqual } from 'node:crypto';
|
|
2
|
+
const TOKEN_COOKIE_PREFIX = 'deptrail_token_';
|
|
3
|
+
export const TOKEN_HEADER = 'x-deptrail-token';
|
|
4
|
+
export function generateToken() {
|
|
5
|
+
return randomBytes(32).toString('hex');
|
|
6
|
+
}
|
|
7
|
+
export function isAllowedHost(hostHeader, port) {
|
|
8
|
+
return hostHeader === `127.0.0.1:${port}` || hostHeader === `localhost:${port}`;
|
|
9
|
+
}
|
|
10
|
+
export function isAllowedOrigin(originHeader, port) {
|
|
11
|
+
return originHeader === `http://127.0.0.1:${port}` || originHeader === `http://localhost:${port}`;
|
|
12
|
+
}
|
|
13
|
+
export function tokensMatch(expected, candidate) {
|
|
14
|
+
if (candidate === undefined)
|
|
15
|
+
return false;
|
|
16
|
+
const expectedBytes = Buffer.from(expected);
|
|
17
|
+
const candidateBytes = Buffer.from(candidate);
|
|
18
|
+
if (expectedBytes.length !== candidateBytes.length)
|
|
19
|
+
return false;
|
|
20
|
+
return timingSafeEqual(expectedBytes, candidateBytes);
|
|
21
|
+
}
|
|
22
|
+
function cookieValue(cookieHeader, name) {
|
|
23
|
+
if (!cookieHeader)
|
|
24
|
+
return undefined;
|
|
25
|
+
for (const part of cookieHeader.split(';')) {
|
|
26
|
+
const separator = part.indexOf('=');
|
|
27
|
+
if (separator === -1)
|
|
28
|
+
continue;
|
|
29
|
+
if (part.slice(0, separator).trim() === name)
|
|
30
|
+
return part.slice(separator + 1).trim();
|
|
31
|
+
}
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
34
|
+
export function tokenCookieName(port) {
|
|
35
|
+
return `${TOKEN_COOKIE_PREFIX}${port}`;
|
|
36
|
+
}
|
|
37
|
+
export function requestToken(request, port) {
|
|
38
|
+
const header = request.headers[TOKEN_HEADER];
|
|
39
|
+
if (typeof header === 'string')
|
|
40
|
+
return header;
|
|
41
|
+
return cookieValue(request.headers.cookie, tokenCookieName(port));
|
|
42
|
+
}
|
|
43
|
+
export function sessionCookie(token, port) {
|
|
44
|
+
return `${tokenCookieName(port)}=${token}; HttpOnly; SameSite=Strict; Path=/`;
|
|
45
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { isObjectId } from "../git/git-commands.js";
|
|
2
|
+
import { isGitRepository } from "../git/git-repository.js";
|
|
3
|
+
import { WORKING_KEY } from "./source-key.js";
|
|
4
|
+
export { WORKING_KEY };
|
|
5
|
+
export const COMMIT_ACTIONS_REASON = 'Actions apply to the working tree. Switch to it to change files.';
|
|
6
|
+
export const COMMIT_RESOLVE_REASON = "Resolve versions runs the package manager with the branch's own configuration, so it is available only in the working tree.";
|
|
7
|
+
export class BranchError extends Error {
|
|
8
|
+
status;
|
|
9
|
+
constructor(status, message) {
|
|
10
|
+
super(message);
|
|
11
|
+
this.name = 'BranchError';
|
|
12
|
+
this.status = status;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
const HEAD_REF = 'HEAD';
|
|
16
|
+
const REF_PREFIX = 'refs/';
|
|
17
|
+
const OPTION_PREFIX = '-';
|
|
18
|
+
function messageOf(failure) {
|
|
19
|
+
return failure instanceof Error ? failure.message : String(failure);
|
|
20
|
+
}
|
|
21
|
+
function isAcceptableRefName(ref) {
|
|
22
|
+
return !ref.startsWith(OPTION_PREFIX) && (ref === HEAD_REF || ref.startsWith(REF_PREFIX));
|
|
23
|
+
}
|
|
24
|
+
export function createBranches(git, sources) {
|
|
25
|
+
function requireRepository() {
|
|
26
|
+
if (!isGitRepository(git))
|
|
27
|
+
throw new BranchError(409, git.unavailable);
|
|
28
|
+
return git;
|
|
29
|
+
}
|
|
30
|
+
async function headCommit(repository) {
|
|
31
|
+
const head = await repository.head();
|
|
32
|
+
if (head === undefined)
|
|
33
|
+
throw new BranchError(409, 'The repository has no commits yet');
|
|
34
|
+
return head.commit;
|
|
35
|
+
}
|
|
36
|
+
async function openCommit(commit) {
|
|
37
|
+
let source;
|
|
38
|
+
try {
|
|
39
|
+
source = await sources.openCommit(commit);
|
|
40
|
+
}
|
|
41
|
+
catch (failure) {
|
|
42
|
+
throw new BranchError(409, messageOf(failure));
|
|
43
|
+
}
|
|
44
|
+
const skipped = source.session.scan.skippedSubmodules ?? [];
|
|
45
|
+
return skipped.length === 0 ? {} : { skippedSubmodules: [...skipped] };
|
|
46
|
+
}
|
|
47
|
+
async function openRef(repository, ref) {
|
|
48
|
+
if (!isAcceptableRefName(ref))
|
|
49
|
+
throw new BranchError(400, 'Invalid ref');
|
|
50
|
+
if (ref === HEAD_REF) {
|
|
51
|
+
const commit = await headCommit(repository);
|
|
52
|
+
return { key: commit, commit, ref, ...(await openCommit(commit)) };
|
|
53
|
+
}
|
|
54
|
+
const listed = (await repository.refs()).refs.find((candidate) => candidate.name === ref);
|
|
55
|
+
if (listed === undefined)
|
|
56
|
+
throw new BranchError(404, `Unknown ref ${ref}`);
|
|
57
|
+
const skipped = await openCommit(listed.commit);
|
|
58
|
+
return { key: listed.commit, commit: listed.commit, ref, committedAt: listed.committedAt, ...skipped };
|
|
59
|
+
}
|
|
60
|
+
async function commitOfKey(repository, key) {
|
|
61
|
+
if (key === WORKING_KEY)
|
|
62
|
+
return headCommit(repository);
|
|
63
|
+
if (isObjectId(key) && sources.commit(key) !== undefined)
|
|
64
|
+
return key;
|
|
65
|
+
throw new BranchError(404, `Unknown source ${key}`);
|
|
66
|
+
}
|
|
67
|
+
async function openMergeBase(repository, base, target) {
|
|
68
|
+
const baseCommit = await commitOfKey(repository, base);
|
|
69
|
+
const targetCommit = await commitOfKey(repository, target);
|
|
70
|
+
const commit = await repository.mergeBase(baseCommit, targetCommit);
|
|
71
|
+
if (commit === undefined)
|
|
72
|
+
throw new BranchError(409, 'No merge base');
|
|
73
|
+
await openCommit(commit);
|
|
74
|
+
return { key: commit, commit };
|
|
75
|
+
}
|
|
76
|
+
return {
|
|
77
|
+
async info() {
|
|
78
|
+
if (!isGitRepository(git))
|
|
79
|
+
return { available: false, reason: git.unavailable, quiet: git.quiet };
|
|
80
|
+
try {
|
|
81
|
+
const [head, listing] = await Promise.all([git.head(), git.refs()]);
|
|
82
|
+
return {
|
|
83
|
+
available: true,
|
|
84
|
+
...(head === undefined ? {} : { head }),
|
|
85
|
+
...(listing.defaultBranch === undefined ? {} : { defaultBranch: listing.defaultBranch }),
|
|
86
|
+
refs: listing.refs,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
catch (failure) {
|
|
90
|
+
return { available: false, reason: messageOf(failure), quiet: false };
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
async open(request) {
|
|
94
|
+
const repository = requireRepository();
|
|
95
|
+
if ('ref' in request)
|
|
96
|
+
return openRef(repository, request.ref);
|
|
97
|
+
return openMergeBase(repository, request.mergeBase.base, request.mergeBase.target);
|
|
98
|
+
},
|
|
99
|
+
sessionOf(key) {
|
|
100
|
+
if (key === WORKING_KEY)
|
|
101
|
+
return sources.working;
|
|
102
|
+
return isObjectId(key) ? sources.commit(key)?.session : undefined;
|
|
103
|
+
},
|
|
104
|
+
};
|
|
105
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { processSecrets } from "../config/secrets.js";
|
|
2
|
+
const LINE_BREAKS = /\s*[\r\n]+\s*/g;
|
|
3
|
+
export function createErrorLog(stream) {
|
|
4
|
+
return (context, failure) => {
|
|
5
|
+
const message = failure instanceof Error ? failure.message : String(failure);
|
|
6
|
+
const line = processSecrets.redact(`${context} failed: ${message}`).replace(LINE_BREAKS, ' ').trim();
|
|
7
|
+
stream.write(`deptrail: ${line}\n`);
|
|
8
|
+
};
|
|
9
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
const DOT_ROOT_SEGMENT = '~';
|
|
2
|
+
export function encodeRootSegment(id) {
|
|
3
|
+
if (id === '.')
|
|
4
|
+
return DOT_ROOT_SEGMENT;
|
|
5
|
+
if (id === DOT_ROOT_SEGMENT)
|
|
6
|
+
return '%7E';
|
|
7
|
+
return encodeURIComponent(id);
|
|
8
|
+
}
|
|
9
|
+
export function decodeRootSegment(rawSegment) {
|
|
10
|
+
if (rawSegment === DOT_ROOT_SEGMENT)
|
|
11
|
+
return '.';
|
|
12
|
+
return decodeURIComponent(rawSegment);
|
|
13
|
+
}
|