Standardized dependency logic implementation (#1998)

This adds a standard logic for handling dependencies between Prism components.

Right now, the download page, the `loadLanguages` function, and the test suite use the new dependency system.
This commit is contained in:
Michael Schmidt 2019-12-16 12:49:21 +01:00 committed by GitHub
parent a7f7009019
commit 7a4a0c7cdd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 857 additions and 194 deletions

File diff suppressed because one or more lines are too long

View File

@ -60,7 +60,7 @@
"css": {
"title": "CSS",
"option": "default",
"peerDependencies": "markup"
"modify": "markup"
},
"clike": {
"title": "C-like",
@ -70,7 +70,7 @@
"javascript": {
"title": "JavaScript",
"require": "clike",
"peerDependencies": "markup",
"modify": "markup",
"alias": "js",
"option": "default"
},
@ -85,7 +85,7 @@
"actionscript": {
"title": "ActionScript",
"require": "javascript",
"peerDependencies": "markup",
"modify": "markup",
"owner": "Golmote"
},
"ada": {
@ -235,6 +235,7 @@
"css-extras": {
"title": "CSS Extras",
"require": "css",
"modify": "css",
"owner": "milesj"
},
"d": {
@ -374,7 +375,7 @@
"haml": {
"title": "Haml",
"require": "ruby",
"peerDependencies": [
"optional": [
"css",
"css-extras",
"coffeescript",
@ -409,8 +410,10 @@
},
"http": {
"title": "HTTP",
"peerDependencies": [
"optional": [
"css",
"javascript",
"json",
"markup"
],
"owner": "danielgtaylor"
@ -455,14 +458,17 @@
"javadoc": {
"title": "JavaDoc",
"require": ["markup", "java", "javadoclike"],
"peerDependencies": [
"modify": [
"java"
],
"optional": [
"scala"
],
"owner": "RunDevelopment"
},
"javadoclike": {
"title": "JavaDoc-like",
"peerDependencies": [
"modify": [
"java",
"javascript",
"php"
@ -485,7 +491,8 @@
"jsdoc": {
"title": "JSDoc",
"require": ["javascript", "javadoclike"],
"peerDependencies": [
"modify": "javascript",
"optional": [
"actionscript",
"coffeescript"
],
@ -494,7 +501,8 @@
"js-extras": {
"title": "JS Extras",
"require": "javascript",
"peerDependencies": [
"modify": "javascript",
"optional": [
"actionscript",
"coffeescript",
"flow",
@ -506,7 +514,8 @@
"js-templates": {
"title": "JS Templates",
"require": "javascript",
"peerDependencies": [
"modify": "javascript",
"optional": [
"css",
"css-extras",
"graphql",
@ -554,7 +563,7 @@
"less": {
"title": "Less",
"require": "css",
"peerDependencies": "css-extras",
"optional": "css-extras",
"owner": "Golmote"
},
"lilypond": {
@ -627,7 +636,7 @@
"n4js": {
"title": "N4JS",
"require": "javascript",
"peerDependencies": [
"optional": [
"jsdoc"
],
"alias": "n4jsd",
@ -670,7 +679,7 @@
"opencl": {
"title": "OpenCL",
"require": "cpp",
"peerDependencies": [
"modify": [
"c",
"cpp"
],
@ -719,11 +728,13 @@
"phpdoc": {
"title": "PHPDoc",
"require": ["php", "javadoclike"],
"modify": "php",
"owner": "RunDevelopment"
},
"php-extras": {
"title": "PHP Extras",
"require": "php",
"modify": "php",
"owner": "milesj"
},
"plsql": {
@ -756,7 +767,7 @@
"pug": {
"title": "Pug",
"require": ["markup", "javascript"],
"peerDependencies": [
"optional": [
"coffeescript",
"ejs",
"handlebars",
@ -775,7 +786,7 @@
},
"pure": {
"title": "Pure",
"peerDependencies": [
"optional": [
"c",
"cpp",
"fortran"
@ -803,7 +814,7 @@
"jsx": {
"title": "React JSX",
"require": ["markup", "javascript"],
"peerDependencies": [
"optional": [
"jsdoc",
"js-extras",
"js-templates"
@ -825,7 +836,7 @@
},
"regex": {
"title": "Regex",
"peerDependencies": [
"modify": [
"actionscript",
"coffeescript",
"flow",
@ -875,7 +886,7 @@
"scss": {
"title": "Sass (Scss)",
"require": "css",
"peerDependencies": "css-extras",
"optional": "css-extras",
"owner": "MoOx"
},
"scala": {
@ -951,7 +962,7 @@
"textile": {
"title": "Textile",
"require": "markup",
"peerDependencies": "css",
"optional": "css",
"owner": "Golmote"
},
"toml": {
@ -979,7 +990,7 @@
"typescript": {
"title": "TypeScript",
"require": "javascript",
"peerDependencies": "js-templates",
"optional": "js-templates",
"alias": "ts",
"owner": "vkbansal"
},
@ -1084,7 +1095,7 @@
"show-invisibles": {
"title": "Show Invisibles",
"description": "Show hidden characters such as tabs and line breaks.",
"after": [
"optional": [
"autolinker",
"data-uri-highlight"
]
@ -1155,7 +1166,7 @@
"title": "Keep Markup",
"description": "Prevents custom markup from being dropped out during highlighting.",
"owner": "Golmote",
"after": "normalize-whitespace",
"optional": "normalize-whitespace",
"noCSS": true
},
"command-line": {
@ -1171,7 +1182,7 @@
"title": "Normalize Whitespace",
"description": "Supports multiple operations to normalize whitespace in code blocks.",
"owner": "zeitgeist87",
"after": "unescaped-markup",
"optional": "unescaped-markup",
"noCSS": true
},
"data-uri-highlight": {

View File

@ -1,82 +1,49 @@
var components = require('../components.js');
var peerDependentsMap = null;
const components = require('../components.js');
const getLoader = require('../dependencies');
function getPeerDependentsMap() {
var peerDependentsMap = {};
Object.keys(components.languages).forEach(function (language) {
if (language === 'meta') {
return false;
}
if (components.languages[language].peerDependencies) {
var peerDependencies = components.languages[language].peerDependencies;
if (!Array.isArray(peerDependencies)) {
peerDependencies = [peerDependencies];
}
peerDependencies.forEach(function (peerDependency) {
if (!peerDependentsMap[peerDependency]) {
peerDependentsMap[peerDependency] = [];
}
peerDependentsMap[peerDependency].push(language);
});
}
});
return peerDependentsMap;
}
function getPeerDependents(mainLanguage) {
if (!peerDependentsMap) {
peerDependentsMap = getPeerDependentsMap();
}
return peerDependentsMap[mainLanguage] || [];
}
/**
* The set of all languages which have been loaded using the below function.
*
* @type {Set<string>}
*/
const loadedLanguages = new Set();
function loadLanguages(arr, withoutDependencies) {
// If no argument is passed, load all components
if (!arr) {
arr = Object.keys(components.languages).filter(function (language) {
return language !== 'meta';
});
}
if (arr && !arr.length) {
return;
/**
* Loads the given languages and adds them to the current Prism instance.
*
* If no languages are provided, __all__ Prism languages will be loaded.
*
* @param {string|string[]} [languages]
* @returns {void}
*/
function loadLanguages(languages) {
if (languages === undefined) {
languages = Object.keys(components.languages).filter(l => l != 'meta');
} else if (!Array.isArray(languages)) {
languages = [languages];
}
if (!Array.isArray(arr)) {
arr = [arr];
}
// the user might have loaded languages via some other way or used `prism.js` which already includes some
// we don't need to validate the ids because `getLoader` will ignore invalid ones
const loaded = [...loadedLanguages, ...Object.keys(Prism.languages)];
arr.forEach(function (language) {
if (!components.languages[language]) {
console.warn('Language does not exist ' + language);
getLoader(components, languages, loaded).load(lang => {
if (!(lang in components.languages)) {
console.warn('Language does not exist: ' + lang);
return;
}
// Load dependencies first
if (!withoutDependencies && components.languages[language].require) {
loadLanguages(components.languages[language].require);
}
var pathToLanguage = './prism-' + language;
const pathToLanguage = './prism-' + lang;
// remove from require cache and from Prism
delete require.cache[require.resolve(pathToLanguage)];
delete Prism.languages[language];
delete Prism.languages[lang];
require(pathToLanguage);
// Reload dependents
var dependents = getPeerDependents(language).filter(function (dependent) {
// If dependent language was already loaded,
// we want to reload it.
if (Prism.languages[dependent]) {
delete Prism.languages[dependent];
return true;
}
return false;
});
if (dependents.length) {
loadLanguages(dependents, true);
}
loadedLanguages.add(lang);
});
}
module.exports = function (arr) {
// Don't expose withoutDependencies
loadLanguages(arr);
};
module.exports = loadLanguages;

431
dependencies.js Normal file
View File

@ -0,0 +1,431 @@
"use strict";
/**
* @typedef {Object<string, ComponentCategory>} Components
* @typedef {Object<string, ComponentEntry | string>} ComponentCategory
*
* @typedef ComponentEntry
* @property {string} [title] The title of the component.
* @property {string} [owner] The GitHub user name of the owner.
* @property {boolean} [noCSS=false] Whether the component doesn't have style sheets which should also be loaded.
* @property {string | string[]} [alias] An optional list of aliases for the id of the component.
* @property {Object<string, string>} [aliasTitles] An optional map from an alias to its title.
*
* Aliases which are not in this map will the get title of the component.
* @property {string | string[]} [optional]
* @property {string | string[]} [require]
* @property {string | string[]} [modify]
*/
var getLoader = (function () {
/**
* A function which does absolutely nothing.
*
* @type {any}
*/
var noop = function () { };
/**
* Converts the given value to an array.
*
* If the given value is already an array, the value itself will be returned.
* `null` and `undefined` will return an empty array.
* For every other value a new array with the given value as its only element will be created.
*
* @param {null | undefined | T | T[]} value
* @returns {T[]}
* @template T
*/
function toArray(value) {
if (Array.isArray(value)) {
return value;
} else {
return value == null ? [] : [value];
}
}
/**
* Returns a new set for the given string array.
*
* @param {string[]} array
* @returns {StringSet}
*
* @typedef {Object<string, true>} StringSet
*/
function toSet(array) {
/** @type {StringSet} */
var set = {};
for (var i = 0, l = array.length; i < l; i++) {
set[array[i]] = true;
}
return set;
}
/**
* Creates a map of every components id to its entry.
*
* @param {Components} components
* @returns {EntryMap}
*
* @typedef {{ [id: string]: Readonly<ComponentEntry> | undefined }} EntryMap
*/
function createEntryMap(components) {
/** @type {EntryMap} */
var map = {};
for (var categoryName in components) {
var category = components[categoryName];
for (var id in category) {
if (id != 'meta') {
/** @type {ComponentEntry | string} */
var entry = category[id];
map[id] = typeof entry == 'string' ? { title: entry } : entry;
}
}
}
return map;
}
/**
* Creates a full dependencies map which includes all types of dependencies and their transitive dependencies.
*
* @param {EntryMap} entryMap
* @returns {DependencyMap}
*
* @typedef {Object<string, StringSet>} DependencyMap
*/
function createDependencyMap(entryMap) {
/** @type {DependencyMap} */
var map = {};
/**
* Adds the dependencies of the given component to the dependency map.
*
* @param {string} id
* @param {string[]} stack
*/
function addToMap(id, stack) {
if (id in map) {
return;
}
stack.push(id);
// check for circular dependencies
var firstIndex = stack.indexOf(id);
if (firstIndex < stack.length - 1) {
throw new Error('Circular dependency: ' + stack.slice(firstIndex).join(' -> '));
}
/** @type {StringSet} */
var dependencies = {};
var entry = entryMap[id];
if (entry) {
/** @type {string[]} */
var deps = [].concat(entry.require, entry.modify, entry.optional).filter(Boolean);
deps.forEach(function (depId) {
if (!(depId in entryMap)) {
throw new Error(id + ' depends on an unknown component ' + depId);
}
addToMap(depId, stack);
dependencies[depId] = true;
for (var transitiveDepId in map[depId]) {
dependencies[transitiveDepId] = true;
}
});
}
map[id] = dependencies;
stack.pop();
}
for (var id in entryMap) {
addToMap(id, []);
}
return map;
}
/**
* Returns a function which resolves the aliases of its given id of alias.
*
* @param {EntryMap} entryMap
* @returns {(idOrAlias: string) => string}
*/
function createAliasResolver(entryMap) {
/** @type {Object<string, string>} */
var map = {};
for (var id in entryMap) {
var entry = entryMap[id];
var aliases = toArray(entry && entry.alias);
aliases.forEach(function (alias) {
if (alias in map) {
throw new Error(alias + ' cannot be alias for both ' + id + ' and ' + map[alias]);
}
if (alias in entryMap) {
throw new Error(alias + ' cannot be alias of ' + id + ' because it is a component.');
}
map[alias] = id;
});
}
return function (idOrAlias) {
return map[idOrAlias] || idOrAlias;
};
}
/**
* @typedef LoadChainer
* @property {(before: T, after: () => T) => T} series
* @property {(values: T[]) => T} parallel
* @template T
*/
/**
* Creates an implicit DAG from the given components and dependencies and call the given `loadComponent` for each
* component in topological order.
*
* @param {DependencyMap} dependencyMap
* @param {StringSet} ids
* @param {(id: string) => T} loadComponent
* @param {LoadChainer<T>} [chainer]
* @returns {T}
* @template T
*/
function loadComponentsInOrder(dependencyMap, ids, loadComponent, chainer) {
const series = chainer ? chainer.series : undefined;
const parallel = chainer ? chainer.parallel : noop;
/** @type {Object<string, T>} */
var cache = {};
/**
* A set of ids of nodes which are not depended upon by any other node in the graph.
* @type {StringSet}
*/
var ends = {};
/**
* Loads the given component and its dependencies or returns the cached value.
*
* @param {string} id
* @returns {T}
*/
function handleId(id) {
if (id in cache) {
return cache[id];
}
// assume that it's an end
// if it isn't, it will be removed later
ends[id] = true;
// all dependencies of the component in the given ids
var dependsOn = [];
for (var depId in dependencyMap[id]) {
if (depId in ids) {
dependsOn.push(depId);
}
}
/**
* The value to be returned.
* @type {T}
*/
var value;
if (dependsOn.length === 0) {
value = loadComponent(id);
} else {
var depsValue = parallel(dependsOn.map(function (depId) {
var value = handleId(depId);
// none of the dependencies can be ends
delete ends[depId];
return value;
}));
if (series) {
// the chainer will be responsibly for calling the function calling loadComponent
value = series(depsValue, function () { return loadComponent(id); });
} else {
// we don't have a chainer, so we call loadComponent ourselves
loadComponent(id);
}
}
// cache and return
return cache[id] = value;
}
for (var id in ids) {
handleId(id);
}
/** @type {T[]} */
var endValues = [];
for (var endId in ends) {
endValues.push(cache[endId]);
}
return parallel(endValues);
}
/**
* Returns whether the given object has any keys.
*
* @param {object} obj
*/
function hasKeys(obj) {
for (var key in obj) {
return true;
}
return false;
}
/**
* Returns an object which provides methods to get the ids of the components which have to be loaded (`getIds`) and
* a way to efficiently load them in synchronously and asynchronous contexts (`load`).
*
* The set of ids to be loaded is a superset of `load`. If some of these ids are in `loaded`, the corresponding
* components will have to reloaded.
*
* The ids in `load` and `loaded` may be in any order and can contain duplicates.
*
* @param {Components} components
* @param {string[]} load
* @param {string[]} [loaded=[]] A list of already loaded components.
*
* If a component is in this list, then all of its requirements will also be assumed to be in the list.
* @returns {Loader}
*
* @typedef Loader
* @property {() => string[]} getIds A function to get all ids of the components to load.
*
* The returned ids will be duplicate-free, alias-free and in load order.
* @property {LoadFunction} load A functional interface to load components.
*
* @typedef {<T> (loadComponent: (id: string) => T, chainer?: LoadChainer<T>) => T} LoadFunction
* A functional interface to load components.
*
* The `loadComponent` function will be called for every component in the order in which they have to be loaded.
*
* The `chainer` is useful for asynchronous loading and its `series` and `parallel` functions can be thought of as
* `Promise#then` and `Promise.all`.
*
* @example
* load(id => { loadComponent(id); }); // returns undefined
*
* await load(
* id => loadComponentAsync(id), // returns a Promise for each id
* {
* series: async (before, after) => {
* await before;
* await after();
* },
* parallel: async (values) => {
* await Promise.all(values);
* }
* }
* );
*/
function getLoader(components, load, loaded) {
var entryMap = createEntryMap(components);
var resolveAlias = createAliasResolver(entryMap);
load = load.map(resolveAlias);
loaded = (loaded || []).map(resolveAlias);
var loadSet = toSet(load);
var loadedSet = toSet(loaded);
// add requirements
load.forEach(addRequirements);
function addRequirements(id) {
var entry = entryMap[id];
if (entry) {
var require = toArray(entry.require);
require.forEach(function (reqId) {
if (!(reqId in loadedSet)) {
loadSet[reqId] = true;
addRequirements(reqId);
}
});
}
}
// add components to reload
// A component x in `loaded` has to be reloaded if
// 1) a component in `load` modifies x.
// 2) x depends on a component in `load`.
// The above two condition have to be applied until nothing changes anymore.
var dependencyMap = createDependencyMap(entryMap);
/** @type {StringSet} */
var loadAdditions = loadSet;
/** @type {StringSet} */
var newIds;
while (hasKeys(loadAdditions)) {
newIds = {};
// condition 1)
for (var loadId in loadAdditions) {
var entry = entryMap[loadId];
if (entry) {
var modify = toArray(entry.modify);
modify.forEach(function (modId) {
if (modId in loadedSet) {
newIds[modId] = true;
}
});
}
}
// condition 2)
for (var loadedId in loadedSet) {
if (!(loadedId in loadSet)) {
for (var depId in dependencyMap[loadedId]) {
if (depId in loadSet) {
newIds[loadedId] = true;
break;
}
}
}
}
loadAdditions = newIds;
for (var newId in loadAdditions) {
loadSet[newId] = true;
}
}
/** @type {Loader} */
var loader = {
getIds: function () {
var ids = [];
loader.load(function (id) {
ids.push(id);
});
return ids;
},
load: function (loadComponent, chainer) {
return loadComponentsInOrder(dependencyMap, loadSet, loadComponent, chainer);
}
};
return loader;
}
return getLoader;
}());
if (typeof module !== 'undefined') {
module.exports = getLoader;
}

View File

@ -175,6 +175,7 @@ section.download {
<script src="scripts/utopia.js"></script>
<script src="prism.js"></script>
<script src="components.js"></script>
<script src="dependencies.js"></script>
<script src="scripts/code.js"></script>
<script src="scripts/vendor/promise.js"></script>
<script src="scripts/vendor/FileSaver.min.js"></script>

View File

@ -7,11 +7,12 @@
"scripts": {
"test:aliases": "mocha tests/aliases-test.js",
"test:core": "mocha tests/core/**/*.js",
"test:dependencies": "mocha tests/dependencies-test.js",
"test:languages": "mocha tests/run.js",
"test:patterns": "mocha tests/pattern-tests.js",
"test:plugins": "mocha tests/plugins/**/*.js",
"test:runner": "mocha tests/testrunner-tests.js",
"test": "npm run test:runner && npm run test:core && npm run test:languages && npm run test:plugins && npm run test:aliases && npm run test:patterns"
"test": "npm run test:runner && npm run test:core && npm run test:dependencies && npm run test:languages && npm run test:plugins && npm run test:aliases && npm run test:patterns"
},
"repository": {
"type": "git",

View File

@ -22,6 +22,21 @@ var treePromise = new Promise(function(resolve) {
});
});
/**
* Converts the given value into an array.
*
* @param {T | T[] | null | undefined} value
* @returns {T[]}
* @template T
*/
function toArray(value) {
if (Array.isArray(value)) {
return value;
} else {
return value == null ? [] : [value];
}
}
var hstr = window.location.hash.match(/(?:languages|plugins)=[-+\w]+|themes=[-\w]+/g);
if (hstr) {
hstr.forEach(function(str) {
@ -50,13 +65,8 @@ if (hstr) {
}
components[category][id].option = 'default';
}
if (components[category][id].require) {
var deps = components[category][id].require;
if (!Array.isArray(deps)) {
deps = [deps];
}
deps.forEach(makeDefault);
}
toArray(components[category][id].require).forEach(makeDefault);
}
}
};
@ -142,9 +152,9 @@ for (var category in components) {
noCSS: all[id].noCSS || all.meta.noCSS,
noJS: all[id].noJS || all.meta.noJS,
enabled: checked,
require: $u.type(all[id].require) === 'string' ? [all[id].require] : all[id].require,
after: $u.type(all[id].after) === 'string' ? [all[id].after] : all[id].after,
peerDependencies: $u.type(all[id].peerDependencies) === 'string' ? [all[id].peerDependencies] : all[id].peerDependencies,
require: toArray(all[id].require),
after: toArray(all[id].after),
modify: toArray(all[id].modify),
owner: all[id].owner,
files: {
minified: {
@ -158,11 +168,9 @@ for (var category in components) {
}
};
if (info.require) {
info.require.forEach(function (v) {
dependencies[v] = (dependencies[v] || []).concat(id);
});
}
info.require.forEach(function (v) {
dependencies[v] = (dependencies[v] || []).concat(id);
});
if (!all[id].noJS && !/\.css$/.test(filepath)) {
info.files.minified.paths.push(filepath.replace(/(\.js)?$/, '.min.js'));
@ -446,72 +454,31 @@ function delayedGenerateCode(){
timerId = setTimeout(generateCode, 500);
}
function getSortedComponents(components, requireName, sorted) {
if (!sorted) {
sorted = [];
for (var component in components) {
sorted.push(component);
}
}
var i = 0;
while (i < sorted.length) {
var id = sorted[i];
var indexOfRequirement = i;
var notNow = false;
for (var requirement in components[id][requireName]) {
indexOfRequirement = sorted.indexOf(components[id][requireName][requirement]);
if (indexOfRequirement > i) {
notNow = true;
break;
}
}
if (notNow) {
var tmp = sorted[i];
sorted[i] = sorted[indexOfRequirement];
sorted[indexOfRequirement] = tmp;
}
else {
i++;
}
}
return sorted;
}
function getSortedComponentsByRequirements(components, afterName) {
var sorted = getSortedComponents(components, afterName);
return getSortedComponents(components, "require", sorted);
}
function generateCode(){
/** @type {CodePromiseInfo[]} */
var promises = [];
var redownload = {};
for (var category in components) {
var all = components[category];
// In case if one component requires other, required component should go first.
var sorted = getSortedComponentsByRequirements(all, category === 'languages' ? 'peerDependencies' : 'after');
for (var i = 0; i < sorted.length; i++) {
var id = sorted[i];
if(id === 'meta') {
for (var id in components[category]) {
if (id === 'meta') {
continue;
}
var info = all[id];
var info = components[category][id];
if (info.enabled) {
if (category !== 'core') {
redownload[category] = redownload[category] || [];
redownload[category] = redownload[category] || [];
redownload[category].push(id);
}
info.files[minified? 'minified' : 'dev'].paths.forEach(function (path) {
info.files[minified ? 'minified' : 'dev'].paths.forEach(function (path) {
if (cache[path]) {
var type = path.match(/\.(\w+)$/)[1];
promises.push({
contentsPromise: cache[path].contentsPromise,
id: id,
category: category,
path: path,
type: type
});
@ -531,7 +498,7 @@ function generateCode(){
var code = res.code;
var errors = res.errors;
if(errors.length) {
if (errors.length) {
error.style.display = 'block';
error.innerHTML = '';
$u.element.contents(error, errors);
@ -571,7 +538,49 @@ function generateCode(){
});
}
/**
* Returns a promise of the code of the Prism bundle.
*
* @param {CodePromiseInfo[]} promises
* @returns {Promise<{ code: { js: string, css: string }, errors: HTMLElement[] }>}
*
* @typedef CodePromiseInfo
* @property {Promise} contentsPromise
* @property {string} id
* @property {string} category
* @property {string} path
* @property {string} type
*/
function buildCode(promises) {
// sort the promises
/** @type {CodePromiseInfo[]} */
var finalPromises = [];
/** @type {Object<string, CodePromiseInfo[]>} */
var toSortMap = {};
promises.forEach(function (p) {
if (p.category == "core" || p.category == "themes") {
finalPromises.push(p);
} else {
var infos = toSortMap[p.id];
if (!infos) {
toSortMap[p.id] = infos = [];
}
infos.push(p);
}
});
// this assumes that the ids in `toSortMap` are complete under transitive requirements
getLoader(components, Object.keys(toSortMap)).getIds().forEach(function (id) {
if (!toSortMap[id]) {
console.error(id + " not found.");
}
finalPromises.push.apply(finalPromises, toSortMap[id]);
});
promises = finalPromises;
// build
var i = 0,
l = promises.length;
var code = {js: '', css: ''};
@ -603,6 +612,9 @@ function buildCode(promises) {
return new Promise(f);
}
/**
* @returns {Promise<string>}
*/
function getVersion() {
return getFileContents('./package.json').then(function (jsonStr) {
return JSON.parse(jsonStr).version;

257
tests/dependencies-test.js Normal file
View File

@ -0,0 +1,257 @@
const { assert } = require('chai');
const getLoader = require('../dependencies');
const components = require('../components');
describe('Dependency logic', function () {
/** @type {import("../dependencies").Components} */
const components = {
languages: {
'a': {
alias: 'a2'
},
'b': {
alias: 'b2'
},
'c': {
require: 'a',
optional: ['b', 'e']
},
'd': {
require: ['c', 'b'],
alias: 'xyz'
},
},
pluginsOrSomething: {
'e': {
modify: 'a'
},
}
};
/**
* Returns the ids of `getLoader`.
*
* @param {string[]} load
* @param {string[]} [loaded]
* @returns {string[]}
*/
function getIds(load, loaded) {
return getLoader(components, load, loaded).getIds();
}
describe('Returned ids', function () {
it('- should load requirements', function () {
assert.sameMembers(getIds(['d']), ['a', 'b', 'c', 'd']);
});
it('- should not load already loaded requirements if not necessary', function () {
assert.sameMembers(getIds(['d'], ['a', 'b']), ['c', 'd']);
});
it('- should load already loaded requirements if requested', function () {
assert.sameMembers(getIds(['a', 'd'], ['a', 'b']), ['a', 'c', 'd']);
});
it('- should reload modified components', function () {
assert.sameMembers(getIds(['e'], ['a', 'b', 'c', 'd']), ['a', 'c', 'd', 'e']);
});
it('- should work with empty load', function () {
assert.sameMembers(getIds([], ['a', 'b', 'c', 'd']), []);
});
it('- should return unknown ids as is', function () {
assert.sameMembers(getIds(['c', 'foo'], ['bar']), ['foo', 'c', 'a']);
});
it('- should throw on unknown dependencies', function () {
assert.throws(() => {
/** @type {import("../dependencies").Components} */
const circular = {
languages: {
a: {
require: 'c'
},
b: 'B'
}
};
getLoader(circular, ['a']).getIds();
});
});
});
describe('Load order', function () {
// Note: The order of a and b isn't defined, so don't add any test with both of them being loaded here
it('- should load components in the correct order (require)', function () {
assert.deepStrictEqual(getIds(['c']), ['a', 'c']);
});
it('- should load components in the correct order (modify)', function () {
assert.deepStrictEqual(getIds(['e', 'a']), ['a', 'e']);
});
it('- should load components in the correct order (optional)', function () {
assert.deepStrictEqual(getIds(['c', 'b'], ['a']), ['b', 'c']);
});
it('- should load components in the correct order (require + optional)', function () {
assert.deepStrictEqual(getIds(['d'], ['a']), ['b', 'c', 'd']);
});
it('- should load components in the correct order (require + modify + optional)', function () {
assert.deepStrictEqual(getIds(['d', 'e'], ['b']), ['a', 'e', 'c', 'd']);
});
});
describe('Aliases', function () {
it('- should resolve aliases in the list of components to load', function () {
assert.sameMembers(getIds(['xyz']), ['a', 'b', 'c', 'd']);
});
it('- should resolve aliases in the list of loaded components', function () {
assert.sameMembers(getIds(['d'], ['a', 'a2', 'b2']), ['c', 'd']);
});
it('- should throw on duplicate aliases', function () {
assert.throws(() => {
/** @type {import("../dependencies").Components} */
const circular = {
languages: {
a: {
alias: 'c'
},
b: {
alias: 'c'
}
}
};
getLoader(circular, ['a']).getIds();
});
});
it('- should throw on aliases which are components', function () {
assert.throws(() => {
/** @type {import("../dependencies").Components} */
const circular = {
languages: {
a: {
alias: 'b'
},
b: 'B'
}
};
getLoader(circular, ['a']).getIds();
});
});
});
describe('Circular dependencies', function () {
it('- should throw on circular dependencies', function () {
assert.throws(() => {
/** @type {import("../dependencies").Components} */
const circular = {
languages: {
a: {
require: 'b'
},
b: {
optional: 'a'
}
}
};
getLoader(circular, ['a']).getIds();
});
});
});
describe('Async loading', function () {
it('- should load components in the correct order', async function () {
/** @type {import("../dependencies").Components} */
const localComponents = {
languages: {
'a': {},
'b': {
require: 'a'
},
'c': {
require: 'b'
}
}
};
/** @type {string[]} */
const actualLoadOrder = [];
/** @type {string[]} */
const actualResolveOrder = [];
/**
*
* @param {string} id
* @returns {Promise<void>}
*/
function loadComp(id) {
actualLoadOrder.push(id);
// the idea is that the components which have to be loaded first, take the longest, so if all were to
// start getting loaded at the same time, their order would be the reverse of the expected order.
let delay;
if (id === 'a') {
delay = 30;
} else if (id === 'b') {
delay = 20;
} else if (id === 'c') {
delay = 10;
}
return new Promise((resolve) => {
setTimeout(() => {
actualResolveOrder.push(id);
resolve();
}, delay);
});
}
const loader = getLoader(localComponents, ['c']);
await loader.load(id => loadComp(id), {
series: async (before, after) => {
await before;
await after();
},
parallel: async (values) => {
await Promise.all(values);
}
});
assert.deepStrictEqual(actualLoadOrder, ['a', 'b', 'c'], 'actualLoadOrder:');
assert.deepStrictEqual(actualResolveOrder, ['a', 'b', 'c'], 'actualResolveOrder:');
});
});
});
describe('components.json', function () {
it('- should be valid', function () {
try {
getLoader(components, Object.keys(components.languages).filter(k => k != 'meta')).getIds();
} catch (error) {
assert.fail(error.toString());
}
});
});

View File

@ -4,9 +4,16 @@ const fs = require("fs");
const vm = require("vm");
const { getAllFiles } = require("./test-discovery");
const components = require("../../components");
const getLoader = require("../../dependencies");
const languagesCatalog = components.languages;
/**
* @typedef PrismLoaderContext
* @property {any} Prism The Prism instance.
* @property {Set<string>} loaded A set of loaded components.
*/
module.exports = {
/**
@ -17,7 +24,7 @@ module.exports = {
*/
createInstance(languages) {
let context = {
loadedLanguages: [],
loaded: new Set(),
Prism: this.createEmptyPrism()
};
@ -27,53 +34,29 @@ module.exports = {
},
/**
* Loads the given languages and appends the config to the given Prism object
* Loads the given languages and appends the config to the given Prism object.
*
* @private
* @param {string|string[]} languages
* @param {{loadedLanguages: string[], Prism: Prism}} context
* @returns {{loadedLanguages: string[], Prism: Prism}}
* @param {PrismLoaderContext} context
* @returns {PrismLoaderContext}
*/
loadLanguages(languages, context) {
if (typeof languages === 'string') {
languages = [languages];
}
for (const language of languages) {
context = this.loadLanguage(language, context);
}
getLoader(components, languages, [...context.loaded]).load(id => {
if (!languagesCatalog[id]) {
throw new Error(`Language '${id}' not found.`);
}
return context;
},
// load the language itself
const languageSource = this.loadComponentSource(id);
context.Prism = this.runFileWithContext(languageSource, { Prism: context.Prism }).Prism;
/**
* Loads the given language (including recursively loading the dependencies) and
* appends the config to the given Prism object
*
* @private
* @param {string} language
* @param {{loadedLanguages: string[], Prism: Prism}} context
* @returns {{loadedLanguages: string[], Prism: Prism}}
*/
loadLanguage(language, context) {
if (!languagesCatalog[language]) {
throw new Error("Language '" + language + "' not found.");
}
// the given language was already loaded
if (-1 < context.loadedLanguages.indexOf(language)) {
return context;
}
// if the language has a dependency -> load it first
if (languagesCatalog[language].require) {
context = this.loadLanguages(languagesCatalog[language].require, context);
}
// load the language itself
const languageSource = this.loadComponentSource(language);
context.Prism = this.runFileWithContext(languageSource, { Prism: context.Prism }).Prism;
context.loadedLanguages.push(language);
context.loaded.add(id);
});
return context;
},