async function getCellsFromModule(importStatements, parentID) {
const imported = new Map();
await Promise.all(
importStatements.map(({ body: { source: { value: id } } }) => {
const url = id.match(/\//) ? id : 'd/' + id;
if (!imported.has(id))
return importModule(`https://api.observablehq.com/${url}.js`).then(
module => {
imported.set(id, module);
}
);
})
);
const nbModules = [];
const funcs = new Map();
for (let j = 0; j < importStatements.length; j++) {
const {
specifiers: funcs,
source: { value: id },
injections
} = importStatements[j].body;
const topModuleID = injections ? `${parentID}/${j + 1}` : id;
let topModule = nbModules.find(({ moduleID }) => moduleID === topModuleID);
if (topModule !== undefined) {
const newFuncNames = [
...new Set(funcs.map(({ imported: { name } }) => name))
];
topModule.funcNames = [
...new Set(topModule.funcNames.concat(newFuncNames))
]; // remove duplicates again
for (let k = 0; k < newFuncNames.length; k++)
topModule.queued.add(newFuncNames[k]);
} // otherwise, we create a new notebook module
else {
const funcNames = [
...new Set(funcs.map(({ imported: { name } }) => name))
];
const queued = new Set(funcNames.concat(LibraryKeys));
const injMap = new Map(
injections ? injections.map(inj => [inj.imported.name, inj]) : []
);
topModule = {
funcNames,
injMap,
queued,
str: [],
moduleID: topModuleID,
module: imported.get(id),
moduleIndex: 0
};
nbModules.push(topModule);
}
}
// process modules / variables
let currModule;
while (
(currModule = nbModules.find(({ funcNames }) => funcNames.length > 0))
) {
const { funcNames, injMap, queued, str, module, moduleIndex } = currModule;
// `funcNames` is a queue that begins with the functions that are imported;
// their dependencies will be added recursively
// `queued` marks the functions that have already been added
while (funcNames.length > 0) {
const name = funcNames.shift(); // could use .pop() here too;
// .shift() just gets the order closer to what the Observable API returns?
if (injMap.has(name)) {
// if this definition is injected from the base notebook using import...with
str.push(` {
from: "${parentID}",
name: "${name}",
remote: "${injMap.get(name).local.name}"
},`);
} else if (name === "FileAttachment") {
throw new Error('FileAttachment API not supported!');
}
// some DOM / Web APIs show up in "inputs" but aren't real cells, we should ignore them
// however, 'define' and 'hljs' are things added to the window by Observable, so we should not ignore those!
// (Probably comment those out if running this in a non-notebook environment)
else if (!(window[name] && name !== 'define' && name !== 'hljs')) {
// WARNING: for cells imported from "broken" notebooks where the same name is given to two different cells,
// the following line grabs the first cell with that name, which differs from current Observable behavior.
const modObj = module.default.modules[moduleIndex].variables.find(
({ name: varName }) => varName === name
);
if (modObj.from) {
// there's an import in your import
// write import variable
str.push(` {
from: "${modObj.from}",
name: "${name}",
remote: "${modObj.remote}"
},`);
// where are you 'from'
if (modObj.from !== parentID) {
// if it's the parent, do nothing
// search in other nbModules
let innerModule = nbModules.find(
({ moduleID }) => moduleID === modObj.from
);
if (innerModule !== undefined) {
// the module is already in nbModules, so check "queued" for the remote name...
if (!innerModule.queued.has(modObj.remote)) {
// not in "queued" so it's not a duplicate
innerModule.funcNames = innerModule.funcNames.push(
modObj.remote
);
innerModule.queued.add(modObj.remote);
}
} else {
// otherwise, we create a new notebook module
// dive into the other modules inside this import
let newModuleIndex = module.default.modules.findIndex(
(mod, ind) =>
ind !== moduleIndex &&
mod.variables.some(
({ name: varName }) => varName === modObj.remote
)
);
if (newModuleIndex > 0) {
// if found, push the remote name and module to nbModules
const newFuncNames = [modObj.remote];
const newQueued = new Set(newFuncNames.concat(LibraryKeys));
innerModule = {
funcNames: newFuncNames,
injMap: new Map(),
queued: newQueued,
str: [],
moduleID: modObj.from,
module,
moduleIndex: newModuleIndex
};
nbModules.push(innerModule);
} else console.log(`not found: ${modObj}`);
// if this happens then probably the import is missing something!
// let the runtime throw with some "could not resolve"
}
}
} else {
// non-import / "normal" variable
if (modObj.inputs) {
// queue the dependencies into this module's "funcNames"
modObj.inputs
.filter(str => !queued.has(str))
.forEach(newName => {
funcNames.push(newName);
queued.add(newName);
});
}
// append the variable definition
str.push(` {
name: "${name}",${
modObj.inputs
? `
inputs: ${JSON.stringify(modObj.inputs)},`
: ''
}
value: ${modObj.value.toString()}
},`);
}
}
}
}
return nbModules;
}