@jupyterlab/services
Version:
Client APIs for the Jupyter services REST APIs
875 lines • 31.1 kB
JavaScript
"use strict";
// Copyright (c) Jupyter Development Team.
// Distributed under the terms of the Modified BSD License.
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.Drive = exports.ContentsManager = exports.Contents = void 0;
const coreutils_1 = require("@jupyterlab/coreutils");
const signaling_1 = require("@lumino/signaling");
const __1 = require("..");
const validate = __importStar(require("./validate"));
/**
* The url for the default drive service.
*/
const SERVICE_DRIVE_URL = 'api/contents';
/**
* The url for the file access.
*/
const FILES_URL = 'files';
/**
* A namespace for contents interfaces.
*/
var Contents;
(function (Contents) {
/**
* Validates an IModel, throwing an error if it does not pass.
*/
function validateContentsModel(contents) {
validate.validateContentsModel(contents);
}
Contents.validateContentsModel = validateContentsModel;
/**
* Validates an ICheckpointModel, throwing an error if it does not pass.
*/
function validateCheckpointModel(checkpoint) {
validate.validateCheckpointModel(checkpoint);
}
Contents.validateCheckpointModel = validateCheckpointModel;
})(Contents || (exports.Contents = Contents = {}));
/**
* A contents manager that passes file operations to the server.
* Multiple servers implementing the `IDrive` interface can be
* attached to the contents manager, so that the same session can
* perform file operations on multiple backends.
*
* This includes checkpointing with the normal file operations.
*/
class ContentsManager {
/**
* Construct a new contents manager object.
*
* @param options - The options used to initialize the object.
*/
constructor(options = {}) {
var _a, _b;
this._isDisposed = false;
this._additionalDrives = new Map();
this._fileChanged = new signaling_1.Signal(this);
const serverSettings = (this.serverSettings =
(_a = options.serverSettings) !== null && _a !== void 0 ? _a : __1.ServerConnection.makeSettings());
this._defaultDrive = (_b = options.defaultDrive) !== null && _b !== void 0 ? _b : new Drive({ serverSettings });
this._defaultDrive.fileChanged.connect(this._onFileChanged, this);
}
/**
* A signal emitted when a file operation takes place.
*/
get fileChanged() {
return this._fileChanged;
}
/**
* Test whether the manager has been disposed.
*/
get isDisposed() {
return this._isDisposed;
}
/**
* Dispose of the resources held by the manager.
*/
dispose() {
if (this.isDisposed) {
return;
}
this._isDisposed = true;
signaling_1.Signal.clearData(this);
}
/**
* Add an `IDrive` to the manager.
*/
addDrive(drive) {
this._additionalDrives.set(drive.name, drive);
drive.fileChanged.connect(this._onFileChanged, this);
}
/**
* Given a path, get a shared model factory from the
* relevant backend. Returns `null` if the backend
* does not provide one.
*/
getSharedModelFactory(path) {
var _a;
const [drive] = this._driveForPath(path);
return (_a = drive === null || drive === void 0 ? void 0 : drive.sharedModelFactory) !== null && _a !== void 0 ? _a : null;
}
/**
* Given a path of the form `drive:local/portion/of/it.txt`
* get the local part of it.
*
* @param path the path.
*
* @returns The local part of the path.
*/
localPath(path) {
const parts = path.split('/');
const firstParts = parts[0].split(':');
if (firstParts.length === 1 || !this._additionalDrives.has(firstParts[0])) {
return coreutils_1.PathExt.removeSlash(path);
}
return coreutils_1.PathExt.join(firstParts.slice(1).join(':'), ...parts.slice(1));
}
/**
* Normalize a global path. Reduces '..' and '.' parts, and removes
* leading slashes from the local part of the path, while retaining
* the drive name if it exists.
*
* @param path the path.
*
* @returns The normalized path.
*/
normalize(path) {
const parts = path.split(':');
if (parts.length === 1) {
return coreutils_1.PathExt.normalize(path);
}
return `${parts[0]}:${coreutils_1.PathExt.normalize(parts.slice(1).join(':'))}`;
}
/**
* Resolve a global path, starting from the root path. Behaves like
* posix-path.resolve, with 3 differences:
* - will never prepend cwd
* - if root has a drive name, the result is prefixed with "<drive>:"
* - before adding drive name, leading slashes are removed
*
* @param path the path.
*
* @returns The normalized path.
*/
resolvePath(root, path) {
const driveName = this.driveName(root);
const localPath = this.localPath(root);
const resolved = coreutils_1.PathExt.resolve('/', localPath, path);
return driveName ? `${driveName}:${resolved}` : resolved;
}
/**
* Given a path of the form `drive:local/portion/of/it.txt`
* get the name of the drive. If the path is missing
* a drive portion, returns an empty string.
*
* @param path the path.
*
* @returns The drive name for the path, or the empty string.
*/
driveName(path) {
const parts = path.split('/');
const firstParts = parts[0].split(':');
if (firstParts.length === 1) {
return '';
}
if (this._additionalDrives.has(firstParts[0])) {
return firstParts[0];
}
return '';
}
/**
* Get a file or directory.
*
* @param path The path to the file.
*
* @param options The options used to fetch the file.
*
* @returns A promise which resolves with the file content.
*/
get(path, options) {
const [drive, localPath] = this._driveForPath(path);
return drive.get(localPath, options).then(contentsModel => {
const listing = [];
if (contentsModel.type === 'directory' && contentsModel.content) {
for (const item of contentsModel.content) {
listing.push({ ...item, path: this._toGlobalPath(drive, item.path) });
}
return {
...contentsModel,
path: this._toGlobalPath(drive, localPath),
content: listing,
serverPath: contentsModel.path
};
}
else {
return {
...contentsModel,
path: this._toGlobalPath(drive, localPath),
serverPath: contentsModel.path
};
}
});
}
/**
* Get an encoded download url given a file path.
*
* @param path - An absolute POSIX file path on the server.
*
* #### Notes
* It is expected that the path contains no relative paths.
*
* The returned URL may include a query parameter.
*/
getDownloadUrl(path) {
const [drive, localPath] = this._driveForPath(path);
return drive.getDownloadUrl(localPath);
}
/**
* Create a new untitled file or directory in the specified directory path.
*
* @param options The options used to create the file.
*
* @returns A promise which resolves with the created file content when the
* file is created.
*/
newUntitled(options = {}) {
if (options.path) {
const globalPath = this.normalize(options.path);
const [drive, localPath] = this._driveForPath(globalPath);
return drive
.newUntitled({ ...options, path: localPath })
.then(contentsModel => {
return {
...contentsModel,
path: coreutils_1.PathExt.join(globalPath, contentsModel.name),
serverPath: contentsModel.path
};
});
}
else {
return this._defaultDrive.newUntitled(options);
}
}
/**
* Delete a file.
*
* @param path - The path to the file.
*
* @returns A promise which resolves when the file is deleted.
*/
delete(path) {
const [drive, localPath] = this._driveForPath(path);
return drive.delete(localPath);
}
/**
* Rename a file or directory.
*
* @param path - The original file path.
*
* @param newPath - The new file path.
*
* @returns A promise which resolves with the new file contents model when
* the file is renamed.
*/
rename(path, newPath) {
const [drive1, path1] = this._driveForPath(path);
const [drive2, path2] = this._driveForPath(newPath);
if (drive1 !== drive2) {
throw Error('ContentsManager: renaming files must occur within a Drive');
}
return drive1.rename(path1, path2).then(contentsModel => {
return {
...contentsModel,
path: this._toGlobalPath(drive1, path2),
serverPath: contentsModel.path
};
});
}
/**
* Save a file.
*
* @param path - The desired file path.
*
* @param options - Optional overrides to the model.
*
* @returns A promise which resolves with the file content model when the
* file is saved.
*
* #### Notes
* Ensure that `model.content` is populated for the file.
*/
save(path, options = {}) {
const globalPath = this.normalize(path);
const [drive, localPath] = this._driveForPath(path);
return drive
.save(localPath, { ...options, path: localPath })
.then(contentsModel => {
return {
...contentsModel,
path: globalPath,
serverPath: contentsModel.path
};
});
}
/**
* Copy a file into a given directory.
*
* @param path - The original file path.
*
* @param toDir - The destination directory path.
*
* @returns A promise which resolves with the new contents model when the
* file is copied.
*
* #### Notes
* The server will select the name of the copied file.
*/
copy(fromFile, toDir) {
const [drive1, path1] = this._driveForPath(fromFile);
const [drive2, path2] = this._driveForPath(toDir);
if (drive1 === drive2) {
return drive1.copy(path1, path2).then(contentsModel => {
return {
...contentsModel,
path: this._toGlobalPath(drive1, contentsModel.path),
serverPath: contentsModel.path
};
});
}
else {
throw Error('Copying files between drives is not currently implemented');
}
}
/**
* Create a checkpoint for a file.
*
* @param path - The path of the file.
*
* @returns A promise which resolves with the new checkpoint model when the
* checkpoint is created.
*/
createCheckpoint(path) {
const [drive, localPath] = this._driveForPath(path);
return drive.createCheckpoint(localPath);
}
/**
* List available checkpoints for a file.
*
* @param path - The path of the file.
*
* @returns A promise which resolves with a list of checkpoint models for
* the file.
*/
listCheckpoints(path) {
const [drive, localPath] = this._driveForPath(path);
return drive.listCheckpoints(localPath);
}
/**
* Restore a file to a known checkpoint state.
*
* @param path - The path of the file.
*
* @param checkpointID - The id of the checkpoint to restore.
*
* @returns A promise which resolves when the checkpoint is restored.
*/
restoreCheckpoint(path, checkpointID) {
const [drive, localPath] = this._driveForPath(path);
return drive.restoreCheckpoint(localPath, checkpointID);
}
/**
* Delete a checkpoint for a file.
*
* @param path - The path of the file.
*
* @param checkpointID - The id of the checkpoint to delete.
*
* @returns A promise which resolves when the checkpoint is deleted.
*/
deleteCheckpoint(path, checkpointID) {
const [drive, localPath] = this._driveForPath(path);
return drive.deleteCheckpoint(localPath, checkpointID);
}
/**
* Given a drive and a local path, construct a fully qualified
* path. The inverse of `_driveForPath`.
*
* @param drive an `IDrive`.
*
* @param localPath the local path on the drive.
*
* @returns the fully qualified path.
*/
_toGlobalPath(drive, localPath) {
if (drive === this._defaultDrive) {
return coreutils_1.PathExt.removeSlash(localPath);
}
else {
return `${drive.name}:${coreutils_1.PathExt.removeSlash(localPath)}`;
}
}
/**
* Given a path, get the `IDrive to which it refers,
* where the path satisfies the pattern
* `'driveName:path/to/file'`. If there is no `driveName`
* prepended to the path, it returns the default drive.
*
* @param path a path to a file.
*
* @returns A tuple containing an `IDrive` object for the path,
* and a local path for that drive.
*/
_driveForPath(path) {
const driveName = this.driveName(path);
const localPath = this.localPath(path);
if (driveName) {
return [this._additionalDrives.get(driveName), localPath];
}
else {
return [this._defaultDrive, localPath];
}
}
/**
* Respond to fileChanged signals from the drives attached to
* the manager. This prepends the drive name to the path if necessary,
* and then forwards the signal.
*/
_onFileChanged(sender, args) {
var _a, _b;
if (sender === this._defaultDrive) {
this._fileChanged.emit(args);
}
else {
let newValue = null;
let oldValue = null;
if ((_a = args.newValue) === null || _a === void 0 ? void 0 : _a.path) {
newValue = {
...args.newValue,
path: this._toGlobalPath(sender, args.newValue.path)
};
}
if ((_b = args.oldValue) === null || _b === void 0 ? void 0 : _b.path) {
oldValue = {
...args.oldValue,
path: this._toGlobalPath(sender, args.oldValue.path)
};
}
this._fileChanged.emit({
type: args.type,
newValue,
oldValue
});
}
}
}
exports.ContentsManager = ContentsManager;
/**
* A default implementation for an `IDrive`, talking to the
* server using the Jupyter REST API.
*/
class Drive {
/**
* Construct a new contents manager object.
*
* @param options - The options used to initialize the object.
*/
constructor(options = {}) {
var _a, _b, _c;
this._isDisposed = false;
this._fileChanged = new signaling_1.Signal(this);
this.name = (_a = options.name) !== null && _a !== void 0 ? _a : 'Default';
this._apiEndpoint = (_b = options.apiEndpoint) !== null && _b !== void 0 ? _b : SERVICE_DRIVE_URL;
this.serverSettings =
(_c = options.serverSettings) !== null && _c !== void 0 ? _c : __1.ServerConnection.makeSettings();
}
/**
* A signal emitted when a file operation takes place.
*/
get fileChanged() {
return this._fileChanged;
}
/**
* Test whether the manager has been disposed.
*/
get isDisposed() {
return this._isDisposed;
}
/**
* Dispose of the resources held by the manager.
*/
dispose() {
if (this.isDisposed) {
return;
}
this._isDisposed = true;
signaling_1.Signal.clearData(this);
}
/**
* Get a file or directory.
*
* @param localPath The path to the file.
*
* @param options The options used to fetch the file.
*
* @returns A promise which resolves with the file content.
*
* Uses the [Jupyter Server API](https://petstore.swagger.io/?url=https://raw.githubusercontent.com/jupyter-server/jupyter_server/main/jupyter_server/services/api/api.yaml#!/contents) and validates the response model.
*/
async get(localPath, options) {
let url = this._getUrl(localPath);
if (options) {
// The notebook type cannot take an format option.
if (options.type === 'notebook') {
delete options['format'];
}
const content = options.content ? '1' : '0';
const hash = options.hash ? '1' : '0';
const params = { ...options, content, hash };
url += coreutils_1.URLExt.objectToQueryString(params);
}
const settings = this.serverSettings;
const response = await __1.ServerConnection.makeRequest(url, {}, settings);
if (response.status !== 200) {
const err = await __1.ServerConnection.ResponseError.create(response);
throw err;
}
const data = await response.json();
validate.validateContentsModel(data);
return data;
}
/**
* Get an encoded download url given a file path.
*
* @param localPath - An absolute POSIX file path on the server.
*
* #### Notes
* It is expected that the path contains no relative paths.
*
* The returned URL may include a query parameter.
*/
getDownloadUrl(localPath) {
const baseUrl = this.serverSettings.baseUrl;
let url = coreutils_1.URLExt.join(baseUrl, FILES_URL, coreutils_1.URLExt.encodeParts(localPath));
let cookie = '';
try {
cookie = document.cookie;
}
catch (e) {
// e.g. SecurityError in case of CSP Sandbox
}
const xsrfTokenMatch = cookie.match('\\b_xsrf=([^;]*)\\b');
if (xsrfTokenMatch) {
const fullUrl = new URL(url);
fullUrl.searchParams.append('_xsrf', xsrfTokenMatch[1]);
url = fullUrl.toString();
}
return Promise.resolve(url);
}
/**
* Create a new untitled file or directory in the specified directory path.
*
* @param options The options used to create the file.
*
* @returns A promise which resolves with the created file content when the
* file is created.
*
* #### Notes
* Uses the [Jupyter Server API](https://petstore.swagger.io/?url=https://raw.githubusercontent.com/jupyter-server/jupyter_server/main/jupyter_server/services/api/api.yaml#!/contents) and validates the response model.
*/
async newUntitled(options = {}) {
var _a;
let body = '{}';
if (options) {
if (options.ext) {
options.ext = Private.normalizeExtension(options.ext);
}
body = JSON.stringify(options);
}
const settings = this.serverSettings;
const url = this._getUrl((_a = options.path) !== null && _a !== void 0 ? _a : '');
const init = {
method: 'POST',
body
};
const response = await __1.ServerConnection.makeRequest(url, init, settings);
if (response.status !== 201) {
const err = await __1.ServerConnection.ResponseError.create(response);
throw err;
}
const data = await response.json();
validate.validateContentsModel(data);
this._fileChanged.emit({
type: 'new',
oldValue: null,
newValue: data
});
return data;
}
/**
* Delete a file.
*
* @param localPath - The path to the file.
*
* @returns A promise which resolves when the file is deleted.
*
* #### Notes
* Uses the [Jupyter Server API](https://petstore.swagger.io/?url=https://raw.githubusercontent.com/jupyter-server/jupyter_server/main/jupyter_server/services/api/api.yaml#!/contents).
*/
async delete(localPath) {
const url = this._getUrl(localPath);
const settings = this.serverSettings;
const init = { method: 'DELETE' };
const response = await __1.ServerConnection.makeRequest(url, init, settings);
// TODO: update IPEP27 to specify errors more precisely, so
// that error types can be detected here with certainty.
if (response.status !== 204) {
const err = await __1.ServerConnection.ResponseError.create(response);
throw err;
}
this._fileChanged.emit({
type: 'delete',
oldValue: { path: localPath },
newValue: null
});
}
/**
* Rename a file or directory.
*
* @param oldLocalPath - The original file path.
*
* @param newLocalPath - The new file path.
*
* @returns A promise which resolves with the new file contents model when
* the file is renamed.
*
* #### Notes
* Uses the [Jupyter Server API](https://petstore.swagger.io/?url=https://raw.githubusercontent.com/jupyter-server/jupyter_server/main/jupyter_server/services/api/api.yaml#!/contents) and validates the response model.
*/
async rename(oldLocalPath, newLocalPath) {
const settings = this.serverSettings;
const url = this._getUrl(oldLocalPath);
const init = {
method: 'PATCH',
body: JSON.stringify({ path: newLocalPath })
};
const response = await __1.ServerConnection.makeRequest(url, init, settings);
if (response.status !== 200) {
const err = await __1.ServerConnection.ResponseError.create(response);
throw err;
}
const data = await response.json();
validate.validateContentsModel(data);
this._fileChanged.emit({
type: 'rename',
oldValue: { path: oldLocalPath },
newValue: data
});
return data;
}
/**
* Save a file.
*
* @param localPath - The desired file path.
*
* @param options - Optional overrides to the model.
*
* @returns A promise which resolves with the file content model when the
* file is saved.
*
* #### Notes
* Ensure that `model.content` is populated for the file.
*
* Uses the [Jupyter Server API](https://petstore.swagger.io/?url=https://raw.githubusercontent.com/jupyter-server/jupyter_server/main/jupyter_server/services/api/api.yaml#!/contents) and validates the response model.
*/
async save(localPath, options = {}) {
const settings = this.serverSettings;
const url = this._getUrl(localPath);
const init = {
method: 'PUT',
body: JSON.stringify(options)
};
const response = await __1.ServerConnection.makeRequest(url, init, settings);
// will return 200 for an existing file and 201 for a new file
if (response.status !== 200 && response.status !== 201) {
const err = await __1.ServerConnection.ResponseError.create(response);
throw err;
}
const data = await response.json();
validate.validateContentsModel(data);
this._fileChanged.emit({
type: 'save',
oldValue: null,
newValue: data
});
return data;
}
/**
* Copy a file into a given directory.
*
* @param localPath - The original file path.
*
* @param toDir - The destination directory path.
*
* @returns A promise which resolves with the new contents model when the
* file is copied.
*
* #### Notes
* The server will select the name of the copied file.
*
* Uses the [Jupyter Server API](https://petstore.swagger.io/?url=https://raw.githubusercontent.com/jupyter-server/jupyter_server/main/jupyter_server/services/api/api.yaml#!/contents) and validates the response model.
*/
async copy(fromFile, toDir) {
const settings = this.serverSettings;
const url = this._getUrl(toDir);
const init = {
method: 'POST',
body: JSON.stringify({ copy_from: fromFile })
};
const response = await __1.ServerConnection.makeRequest(url, init, settings);
if (response.status !== 201) {
const err = await __1.ServerConnection.ResponseError.create(response);
throw err;
}
const data = await response.json();
validate.validateContentsModel(data);
this._fileChanged.emit({
type: 'new',
oldValue: null,
newValue: data
});
return data;
}
/**
* Create a checkpoint for a file.
*
* @param localPath - The path of the file.
*
* @returns A promise which resolves with the new checkpoint model when the
* checkpoint is created.
*
* #### Notes
* Uses the [Jupyter Server API](https://petstore.swagger.io/?url=https://raw.githubusercontent.com/jupyter-server/jupyter_server/main/jupyter_server/services/api/api.yaml#!/contents) and validates the response model.
*/
async createCheckpoint(localPath) {
const url = this._getUrl(localPath, 'checkpoints');
const init = { method: 'POST' };
const response = await __1.ServerConnection.makeRequest(url, init, this.serverSettings);
if (response.status !== 201) {
const err = await __1.ServerConnection.ResponseError.create(response);
throw err;
}
const data = await response.json();
validate.validateCheckpointModel(data);
return data;
}
/**
* List available checkpoints for a file.
*
* @param localPath - The path of the file.
*
* @returns A promise which resolves with a list of checkpoint models for
* the file.
*
* #### Notes
* Uses the [Jupyter Server API](https://petstore.swagger.io/?url=https://raw.githubusercontent.com/jupyter-server/jupyter_server/main/jupyter_server/services/api/api.yaml#!/contents) and validates the response model.
*/
async listCheckpoints(localPath) {
const url = this._getUrl(localPath, 'checkpoints');
const response = await __1.ServerConnection.makeRequest(url, {}, this.serverSettings);
if (response.status !== 200) {
const err = await __1.ServerConnection.ResponseError.create(response);
throw err;
}
const data = await response.json();
if (!Array.isArray(data)) {
throw new Error('Invalid Checkpoint list');
}
for (let i = 0; i < data.length; i++) {
validate.validateCheckpointModel(data[i]);
}
return data;
}
/**
* Restore a file to a known checkpoint state.
*
* @param localPath - The path of the file.
*
* @param checkpointID - The id of the checkpoint to restore.
*
* @returns A promise which resolves when the checkpoint is restored.
*
* #### Notes
* Uses the [Jupyter Server API](https://petstore.swagger.io/?url=https://raw.githubusercontent.com/jupyter-server/jupyter_server/main/jupyter_server/services/api/api.yaml#!/contents).
*/
async restoreCheckpoint(localPath, checkpointID) {
const url = this._getUrl(localPath, 'checkpoints', checkpointID);
const init = { method: 'POST' };
const response = await __1.ServerConnection.makeRequest(url, init, this.serverSettings);
if (response.status !== 204) {
const err = await __1.ServerConnection.ResponseError.create(response);
throw err;
}
}
/**
* Delete a checkpoint for a file.
*
* @param localPath - The path of the file.
*
* @param checkpointID - The id of the checkpoint to delete.
*
* @returns A promise which resolves when the checkpoint is deleted.
*
* #### Notes
* Uses the [Jupyter Server API](https://petstore.swagger.io/?url=https://raw.githubusercontent.com/jupyter-server/jupyter_server/main/jupyter_server/services/api/api.yaml#!/contents).
*/
async deleteCheckpoint(localPath, checkpointID) {
const url = this._getUrl(localPath, 'checkpoints', checkpointID);
const init = { method: 'DELETE' };
const response = await __1.ServerConnection.makeRequest(url, init, this.serverSettings);
if (response.status !== 204) {
const err = await __1.ServerConnection.ResponseError.create(response);
throw err;
}
}
/**
* Get a REST url for a file given a path.
*/
_getUrl(...args) {
const parts = args.map(path => coreutils_1.URLExt.encodeParts(path));
const baseUrl = this.serverSettings.baseUrl;
return coreutils_1.URLExt.join(baseUrl, this._apiEndpoint, ...parts);
}
}
exports.Drive = Drive;
/**
* A namespace for module private data.
*/
var Private;
(function (Private) {
/**
* Normalize a file extension to be of the type `'.foo'`.
*
* Adds a leading dot if not present and converts to lower case.
*/
function normalizeExtension(extension) {
if (extension.length > 0 && extension.indexOf('.') !== 0) {
extension = `.${extension}`;
}
return extension;
}
Private.normalizeExtension = normalizeExtension;
})(Private || (Private = {}));
//# sourceMappingURL=index.js.map