Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions demo/foo/index.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
console.log(`foo`);
5 changes: 5 additions & 0 deletions demo/index.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import './foo/index.mjs';
import hash from './foo/index-sha512.mjs';

console.log(`demo`);
console.log(`demo hash:`, hash);
50 changes: 50 additions & 0 deletions demo/loader-sha512.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import crypto from 'crypto';
import path from 'path';

const shaRegExp = /-sha512(\.mjs)$/;

function getSourcePath(p) {
if (p.protocol !== `file:`)
return p;

const pString = p.toString();
const pFixed = pString.replace(shaRegExp, `$1`);
if (pFixed === pString)
return p;

return new URL(pFixed);
}

export function getFileSystem(defaultGetFileSystem) {
const fileSystem = defaultGetFileSystem();

return {
readFileSync(p) {
const fixedP = getSourcePath(p);
if (fixedP === p)
return fileSystem.readFileSync(p);

const content = fileSystem.readFileSync(fixedP);
const hash = crypto.createHash(`sha512`).update(content).digest(`hex`);

return Buffer.from(`export default ${JSON.stringify(hash)};`);
},

statEntrySync(p) {
const fixedP = getSourcePath(p);
return fileSystem.statEntrySync(fixedP);
},

realpathSync(p) {
Comment thread
bmeck marked this conversation as resolved.
const fixedP = getSourcePath(p);
if (fixedP === p)
return fileSystem.realpathSync(p);

const realpath = fileSystem.realpathSync(fixedP);
if (path.extname(realpath) !== `.mjs`)
throw new Error(`Paths must be .mjs extension to go through the sha512 loader`);

return realpath.replace(/\.mjs$/, `-sha512.mjs`);
},
};
}
1 change: 1 addition & 0 deletions lib/internal/errors.js
Original file line number Diff line number Diff line change
Expand Up @@ -1342,6 +1342,7 @@ E('ERR_IPC_CHANNEL_CLOSED', 'Channel closed', Error);
E('ERR_IPC_DISCONNECTED', 'IPC channel is already disconnected', Error);
E('ERR_IPC_ONE_PIPE', 'Child process can have only one IPC pipe', Error);
E('ERR_IPC_SYNC_FORK', 'IPC cannot be used with synchronous forks', Error);
E('ERR_LOADER_MISSING_SYNC_FS', 'Missing synchronous filesystem implementation of a loader', Error);
E('ERR_MANIFEST_ASSERT_INTEGRITY',
(moduleURL, realIntegrities) => {
let msg = `The content of "${
Expand Down
62 changes: 62 additions & 0 deletions lib/internal/modules/esm/get_file_system.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
'use strict';

const {
ObjectAssign,
ObjectCreate,
SafeMap,
} = primordials;

const realpathCache = new SafeMap();

const internalFS = require('internal/fs/utils');
const fs = require('fs');
const fsPromises = require('internal/fs/promises').exports;
const packageJsonReader = require('internal/modules/package_json_reader');
const { fileURLToPath } = require('url');
const { internalModuleStat } = internalBinding('fs');

const defaultFileSystem = {
async readFile(p) {
return fsPromises.readFile(p);
},

async statEntry(p) {
return internalModuleStat(fileURLToPath(p));
},

async readJson(p) {
return packageJsonReader.read(fileURLToPath(p));
},

async realpath(p) {
return fsPromises.realpath(p, {
[internalFS.realpathCacheKey]: realpathCache
});
},

readFileSync(p) {
return fs.readFileSync(p);
},

statEntrySync(p) {
return internalModuleStat(fileURLToPath(p));
},

readJsonSync(p) {
return packageJsonReader.read(fileURLToPath(p));
},

realpathSync(p) {
return fs.realpathSync(p, {
[internalFS.realpathCacheKey]: realpathCache
});
},
};

function defaultGetFileSystem(defaultGetFileSystem) {
return ObjectAssign(ObjectCreate(null), defaultFileSystem);
}

module.exports = {
defaultGetFileSystem,
};
6 changes: 3 additions & 3 deletions lib/internal/modules/esm/get_source.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,28 +5,28 @@ const {
decodeURIComponent,
} = primordials;
const { getOptionValue } = require('internal/options');
const esmLoader = require('internal/process/esm_loader');

// Do not eagerly grab .manifest, it may be in TDZ
const policy = getOptionValue('--experimental-policy') ?
require('internal/process/policy') :
null;

const { Buffer } = require('buffer');

const fs = require('internal/fs/promises').exports;
const { URL } = require('internal/url');
const {
ERR_INVALID_URL,
ERR_INVALID_URL_SCHEME,
} = require('internal/errors').codes;
const readFileAsync = fs.readFile;

const DATA_URL_PATTERN = /^[^/]+\/[^,;]+(?:[^,]*?)(;base64)?,([\s\S]*)$/;

async function defaultGetSource(url, { format } = {}, defaultGetSource) {
const parsed = new URL(url);
let source;
if (parsed.protocol === 'file:') {
source = await readFileAsync(parsed);
source = await esmLoader.getFileSystem().readFile(parsed);
} else if (parsed.protocol === 'data:') {
const match = RegExpPrototypeExec(DATA_URL_PATTERN, parsed.pathname);
if (!match) {
Expand Down
83 changes: 82 additions & 1 deletion lib/internal/modules/esm/loader.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,22 @@ require('internal/modules/cjs/loader');
const {
Array,
ArrayIsArray,
ArrayPrototypeFilter,
ArrayPrototypeJoin,
ArrayPrototypePush,
ArrayPrototypeSlice,
FunctionPrototypeBind,
FunctionPrototypeCall,
ObjectAssign,
ObjectCreate,
ObjectKeys,
ObjectPrototypeHasOwnProperty,
ObjectSetPrototypeOf,
PromiseAll,
RegExpPrototypeExec,
SafeArrayIterator,
SafeWeakMap,
StringPrototypeEndsWith,
globalThis,
} = primordials;
const { MessageChannel } = require('internal/worker/io');
Expand All @@ -27,7 +32,8 @@ const {
ERR_INVALID_MODULE_SPECIFIER,
ERR_INVALID_RETURN_PROPERTY_VALUE,
ERR_INVALID_RETURN_VALUE,
ERR_UNKNOWN_MODULE_FORMAT
ERR_UNKNOWN_MODULE_FORMAT,
ERR_LOADER_MISSING_SYNC_FS
} = require('internal/errors').codes;
const { pathToFileURL, isURLInstance } = require('internal/url');
const {
Expand All @@ -45,6 +51,9 @@ const {
initializeImportMeta
} = require('internal/modules/esm/initialize_import_meta');
const { defaultLoad } = require('internal/modules/esm/load');
const {
defaultGetFileSystem
} = require('internal/modules/esm/get_file_system');
const { translators } = require(
'internal/modules/esm/translators');
const { getOptionValue } = require('internal/options');
Expand Down Expand Up @@ -81,6 +90,14 @@ class ESMLoader {
defaultResolve,
];

/**
* @private
* @property {Function[]} fileSystemBuilders First-in-first-out list of file system utilities compositors
*/
#fileSystemBuilders = [
defaultGetFileSystem,
];

#importMetaInitializer = initializeImportMeta;

/**
Expand All @@ -104,6 +121,7 @@ class ESMLoader {
translators = translators;

static pluckHooks({
getFileSystem,
globalPreload,
resolve,
load,
Expand Down Expand Up @@ -159,10 +177,17 @@ class ESMLoader {
if (load) {
acceptedHooks.loader = FunctionPrototypeBind(load, null);
}
if (getFileSystem) {
acceptedHooks.getFileSystem = FunctionPrototypeBind(getFileSystem, null);
}

return acceptedHooks;
}

constructor() {
this.buildFileSystem();
}

/**
* Collect custom/user-defined hook(s). After all hooks have been collected,
* calls global preload hook(s).
Expand All @@ -180,6 +205,7 @@ class ESMLoader {
globalPreloader,
resolver,
loader,
getFileSystem,
} = ESMLoader.pluckHooks(exports);

if (globalPreloader) ArrayPrototypePush(
Expand All @@ -194,13 +220,68 @@ class ESMLoader {
this.#loaders,
FunctionPrototypeBind(loader, null), // [1]
);
if (getFileSystem) ArrayPrototypePush(
this.#fileSystemBuilders,
FunctionPrototypeBind(getFileSystem, null), // [1]
);
}

// [1] ensure hook function is not bound to ESMLoader instance

this.buildFileSystem();
this.preload();
}

buildFileSystem() {
// Note: makes assumptions as to how chaining will work to demonstrate
// the capability; subject to change once chaining's API is finalized.
const fileSystemFactories = ArrayPrototypeSlice(this.#fileSystemBuilders);

const defaultFileSystemFactory = fileSystemFactories[0];
let finalFileSystem =
defaultFileSystemFactory();

const asyncKeys = ArrayPrototypeFilter(
ObjectKeys(finalFileSystem),
(name) => !StringPrototypeEndsWith(name, 'Sync'),
);

for (let i = 1; i < fileSystemFactories.length; ++i) {
const currentFileSystem = finalFileSystem;
const fileSystem = fileSystemFactories[i](() => currentFileSystem);

// If the loader specifies a sync hook but omits the async one we
// leverage the sync version by default, so that most hook authors
// don't have to write their implementations twice.
for (let j = 0; j < asyncKeys.length; ++j) {
const asyncKey = asyncKeys[j];
const syncKey = `${asyncKey}Sync`;

const hasAsync = ObjectPrototypeHasOwnProperty(fileSystem, asyncKey);
const hasSync = ObjectPrototypeHasOwnProperty(fileSystem, syncKey);

if (
!hasAsync &&
hasSync
) {
fileSystem[asyncKey] = async (...args) => {
return fileSystem[syncKey](...args);
};
} else if (!hasSync && hasAsync) {
throw new ERR_LOADER_MISSING_SYNC_FS();
}
}

finalFileSystem = ObjectAssign(
ObjectCreate(null),
currentFileSystem,
fileSystem,
);
}

this.fileSystem = finalFileSystem;
}

async eval(
source,
url = pathToFileURL(`${process.cwd()}/[eval${++this.evalIndex}]`).href
Expand Down
Loading