@jsenv/core
Version:
Tool to develop, test and build js projects
766 lines (727 loc) • 21.9 kB
JavaScript
import { generateUrlForInlineContent } from "@jsenv/ast";
import { generateContentFrame } from "@jsenv/humanize";
import {
asSpecifierWithoutSearch,
getCallerPosition,
stringifyUrlSite,
urlToBasename,
urlToExtension,
} from "@jsenv/urls";
import { prependContent } from "../prepend_content.js";
let referenceId = 0;
export const createDependencies = (ownerUrlInfo) => {
const { referenceToOthersSet } = ownerUrlInfo;
const startCollecting = async (callback) => {
const prevReferenceToOthersSet = new Set(referenceToOthersSet);
referenceToOthersSet.clear();
const stopCollecting = () => {
for (const prevReferenceToOther of prevReferenceToOthersSet) {
checkForDependencyRemovalEffects(prevReferenceToOther);
}
prevReferenceToOthersSet.clear();
};
try {
await callback();
} finally {
// finally to ensure reference are updated even in case of error
stopCollecting();
}
};
const createResolveAndFinalize = (props) => {
const originalReference = createReference({
ownerUrlInfo,
...props,
});
const reference = originalReference.resolve();
if (reference.urlInfo) {
return reference;
}
const kitchen = ownerUrlInfo.kitchen;
const urlInfo = kitchen.graph.reuseOrCreateUrlInfo(reference);
reference.urlInfo = urlInfo;
addDependency(reference);
ownerUrlInfo.context.finalizeReference(reference);
return reference;
};
const found = ({ trace, ...rest }) => {
if (trace === undefined) {
trace = traceFromUrlSite(
adjustUrlSite(ownerUrlInfo, {
url: ownerUrlInfo.url,
line: rest.specifierLine,
column: rest.specifierColumn,
}),
);
}
const reference = createResolveAndFinalize({
trace,
...rest,
});
return reference;
};
const foundInline = ({
isOriginalPosition,
specifierLine,
specifierColumn,
content,
...rest
}) => {
const parentUrl = isOriginalPosition
? ownerUrlInfo.url
: ownerUrlInfo.generatedUrl;
const parentContent = isOriginalPosition
? ownerUrlInfo.originalContent
: ownerUrlInfo.content;
const trace = traceFromUrlSite({
url: parentUrl,
content: parentContent,
line: specifierLine,
column: specifierColumn,
});
const reference = createResolveAndFinalize({
trace,
isOriginalPosition,
specifierLine,
specifierColumn,
isInline: true,
content,
...rest,
});
return reference;
};
// side effect file
const foundSideEffectFile = async ({ sideEffectFileUrl, trace, ...rest }) => {
if (trace === undefined) {
const { url, line, column } = getCallerPosition();
trace = traceFromUrlSite({
url,
line,
column,
});
}
const sideEffectFileReference = ownerUrlInfo.dependencies.inject({
trace,
type: "side_effect_file",
specifier: sideEffectFileUrl,
...rest,
});
const injectAsBannerCodeBeforeFinalize = (urlInfoReceiver) => {
const basename = urlToBasename(sideEffectFileUrl);
const inlineUrl = generateUrlForInlineContent({
url: urlInfoReceiver.originalUrl || urlInfoReceiver.url,
basename,
extension: urlToExtension(sideEffectFileUrl),
});
const sideEffectFileReferenceInlined = sideEffectFileReference.inline({
ownerUrlInfo: urlInfoReceiver,
trace,
type: "side_effect_file",
specifier: inlineUrl,
});
urlInfoReceiver.addContentTransformationCallback(async () => {
await sideEffectFileReferenceInlined.urlInfo.cook();
await prependContent(
urlInfoReceiver,
sideEffectFileReferenceInlined.urlInfo,
);
});
};
// When possible we inject code inside the file in a common ancestor
// -> less duplication
// During dev:
// during dev cooking files is incremental
// so HTML/JS is already executed by the browser
// we can't late inject into entry point
// During build:
// files are not executed so it's possible to inject reference
// when discovering a side effect file
const visitedMap = new Map();
let foundOrInjectedOnce = false;
const visit = (urlInfo) => {
urlInfo = urlInfo.findParentIfInline() || urlInfo;
const value = visitedMap.get(urlInfo);
if (value !== undefined) {
return value;
}
// search if already referenced
for (const referenceToOther of urlInfo.referenceToOthersSet) {
if (referenceToOther === sideEffectFileReference) {
continue;
}
if (referenceToOther.url === sideEffectFileUrl) {
// consider this reference becomes the last reference
// this ensure this ref is properly detected as inlined by urlInfo.isUsed()
sideEffectFileReference.next =
referenceToOther.next || referenceToOther;
foundOrInjectedOnce = true;
visitedMap.set(urlInfo, true);
return true;
}
if (
referenceToOther.original &&
referenceToOther.original.url === sideEffectFileUrl
) {
// consider this reference becomes the last reference
// this ensure this ref is properly detected as inlined by urlInfo.isUsed()
sideEffectFileReference.next =
referenceToOther.next || referenceToOther;
foundOrInjectedOnce = true;
visitedMap.set(urlInfo, true);
return true;
}
}
// not referenced and we reach an entry point, stop there
if (urlInfo.isEntryPoint) {
foundOrInjectedOnce = true;
visitedMap.set(urlInfo, true);
injectAsBannerCodeBeforeFinalize(urlInfo);
return true;
}
visitedMap.set(urlInfo, false);
for (const referenceFromOther of urlInfo.referenceFromOthersSet) {
const urlInfoReferencingThisOne = referenceFromOther.ownerUrlInfo;
visit(urlInfoReferencingThisOne);
// during dev the first urlInfo where we inject the side effect file is enough
// during build we want to inject into every possible entry point
if (foundOrInjectedOnce && urlInfo.context.dev) {
break;
}
}
return false;
};
visit(ownerUrlInfo);
if (ownerUrlInfo.context.dev && !foundOrInjectedOnce) {
injectAsBannerCodeBeforeFinalize(
ownerUrlInfo.findParentIfInline() || ownerUrlInfo,
);
}
};
const inject = ({ trace, ...rest }) => {
if (trace === undefined) {
const { url, line, column } = getCallerPosition();
trace = traceFromUrlSite({
url,
line,
column,
});
}
const reference = createResolveAndFinalize({
trace,
injected: true,
...rest,
});
return reference;
};
return {
startCollecting,
createResolveAndFinalize,
found,
foundInline,
foundSideEffectFile,
inject,
};
};
/*
* - "http_request"
* - "entry_point"
* - "link_href"
* - "style"
* - "script"
* - "a_href"
* - "iframe_src
* - "img_src"
* - "img_srcset"
* - "source_src"
* - "source_srcset"
* - "image_href"
* - "use_href"
* - "css_@import"
* - "css_url"
* - "js_import"
* - "js_import_script"
* - "js_url"
* - "js_inline_content"
* - "sourcemap_comment"
* - "webmanifest_icon_src"
* - "package_json"
* - "side_effect_file"
* */
const createReference = ({
ownerUrlInfo,
data = {},
trace,
type,
subtype,
expectedContentType,
expectedType,
expectedSubtype,
filenameHint,
integrity,
crossorigin,
specifier,
specifierStart,
specifierEnd,
specifierLine,
specifierColumn,
baseUrl,
isOriginalPosition,
isEntryPoint = false,
isDynamicEntryPoint = false,
isResourceHint = false,
// implicit references are not real references
// they represent an abstract relationship
isImplicit = false,
// weak references cannot keep the corresponding url info alive
// there must be an other reference to keep the url info alive
// an url referenced solely by weak references is:
// - not written in build directory
// - can be removed from graph during dev/build
// - not cooked until referenced by a strong reference
isWeak = false,
hasVersioningEffect = false,
version = null,
injected = false,
isInline = false,
content,
contentType,
fsStat = null,
debug = false,
original = null,
prev = null,
next = null,
url = null,
searchParams = null,
generatedUrl = null,
generatedSpecifier = null,
urlInfo = null,
escape = null,
importAttributes,
isSideEffectImport = false,
astInfo = {},
mutation,
}) => {
if (typeof specifier !== "string") {
if (specifier instanceof URL) {
specifier = specifier.href;
} else {
throw new TypeError(
`"specifier" must be a string, got ${specifier} in ${ownerUrlInfo.url}`,
);
}
}
const reference = {
id: ++referenceId,
ownerUrlInfo,
original,
prev,
next,
data,
trace,
url,
urlInfo,
searchParams,
generatedUrl,
generatedSpecifier,
type,
subtype,
expectedContentType,
expectedType,
expectedSubtype,
filenameHint,
integrity,
crossorigin,
specifier,
get specifierPathname() {
return asSpecifierWithoutSearch(reference.specifier);
},
specifierStart,
specifierEnd,
specifierLine,
specifierColumn,
isOriginalPosition,
baseUrl,
isEntryPoint,
isDynamicEntryPoint,
isResourceHint,
isImplicit,
implicitReferenceSet: new Set(),
isWeak,
hasVersioningEffect,
urlInfoEffectSet: new Set(),
version,
injected,
timing: {},
fsStat,
debug,
// for inline resources the reference contains the content
isInline,
content,
contentType,
escape,
// used mostly by worker and import assertions
astInfo,
importAttributes,
isSideEffectImport,
mutation,
};
reference.resolve = () => {
const resolvedReference =
reference.ownerUrlInfo.context.resolveReference(reference);
return resolvedReference;
};
reference.redirect = (url, props = {}) => {
const redirectedProps = getRedirectedReferenceProps(reference, url);
const referenceRedirected = createReference({
...redirectedProps,
...props,
});
reference.next = referenceRedirected;
return referenceRedirected;
};
// "formatReference" can be async BUT this is an exception
// for most cases it will be sync. We want to favor the sync signature to keep things simpler
// The only case where it needs to be async is when
// the specifier is a `data:*` url
// in this case we'll wait for the promise returned by
// "formatReference"
reference.readGeneratedSpecifier = () => {
if (reference.generatedSpecifier.then) {
return reference.generatedSpecifier.then((value) => {
reference.generatedSpecifier = value;
return value;
});
}
return reference.generatedSpecifier;
};
reference.inline = ({
line,
column,
// when urlInfo is given it means reference is moved into an other file
ownerUrlInfo = reference.ownerUrlInfo,
...props
}) => {
const content =
ownerUrlInfo === undefined
? isOriginalPosition
? reference.ownerUrlInfo.originalContent
: reference.ownerUrlInfo.content
: ownerUrlInfo.content;
const trace = traceFromUrlSite({
url:
ownerUrlInfo === undefined
? isOriginalPosition
? reference.ownerUrlInfo.url
: reference.ownerUrlInfo.generatedUrl
: reference.ownerUrlInfo.url,
content,
line,
column,
});
const inlineCopy = ownerUrlInfo.dependencies.createResolveAndFinalize({
isInline: true,
original: reference.original || reference,
prev: reference,
trace,
injected: reference.injected,
expectedType: reference.expectedType,
...props,
});
// the previous reference stays alive so that even after inlining
// updating the file will invalidate the other file where it was inlined
reference.next = inlineCopy;
return inlineCopy;
};
reference.addImplicit = (props) => {
const implicitReference = ownerUrlInfo.dependencies.inject({
...props,
isImplicit: true,
});
reference.implicitReferenceSet.add(implicitReference);
return implicitReference;
};
reference.gotInlined = () => {
return !reference.isInline && reference.next && reference.next.isInline;
};
reference.remove = () => removeDependency(reference);
// Object.preventExtensions(reference) // useful to ensure all properties are declared here
return reference;
};
const addDependency = (reference) => {
const { ownerUrlInfo } = reference;
if (ownerUrlInfo.referenceToOthersSet.has(reference)) {
return;
}
if (!canAddOrRemoveReference(reference)) {
throw new Error(
`cannot add reference for content already sent to the browser
--- reference url ---
${reference.url}
--- content url ---
${ownerUrlInfo.url}`,
);
}
ownerUrlInfo.referenceToOthersSet.add(reference);
if (reference.isImplicit) {
// an implicit reference is a reference that does not explicitely appear in the file
// but has an impact on the file
// -> package.json on import resolution for instance
// in that case:
// - file depends on the implicit file (it must autoreload if package.json is modified)
// - cache validity for the file depends on the implicit file (it must be re-cooked if package.json is modified)
ownerUrlInfo.implicitUrlSet.add(reference.url);
if (ownerUrlInfo.isInline) {
const parentUrlInfo = ownerUrlInfo.graph.getUrlInfo(
ownerUrlInfo.inlineUrlSite.url,
);
parentUrlInfo.implicitUrlSet.add(reference.url);
}
}
const referencedUrlInfo = reference.urlInfo;
referencedUrlInfo.referenceFromOthersSet.add(reference);
applyReferenceEffectsOnUrlInfo(reference);
for (const implicitRef of reference.implicitReferenceSet) {
addDependency(implicitRef);
}
};
const removeDependency = (reference) => {
const { ownerUrlInfo } = reference;
if (!ownerUrlInfo.referenceToOthersSet.has(reference)) {
return false;
}
if (!canAddOrRemoveReference(reference)) {
throw new Error(
`cannot remove reference for content already sent to the browser
--- reference url ---
${reference.url}
--- content url ---
${ownerUrlInfo.url}`,
);
}
for (const implicitRef of reference.implicitReferenceSet) {
implicitRef.remove();
}
ownerUrlInfo.referenceToOthersSet.delete(reference);
return checkForDependencyRemovalEffects(reference);
};
const canAddOrRemoveReference = (reference) => {
if (reference.isWeak || reference.isImplicit) {
// weak and implicit references have no restrictions
// because they are not actual references with an influence on content
return true;
}
const { ownerUrlInfo } = reference;
if (ownerUrlInfo.context.build) {
// during build url content is not executed
// it's still possible to mutate references safely
return true;
}
if (!ownerUrlInfo.contentFinalized) {
return true;
}
if (ownerUrlInfo.isRoot) {
// the root urlInfo is abstract, there is no real file behind it
return true;
}
if (reference.type === "http_request") {
// reference created to http requests are abstract concepts
return true;
}
return false;
};
const checkForDependencyRemovalEffects = (reference) => {
const { ownerUrlInfo } = reference;
const { referenceToOthersSet } = ownerUrlInfo;
if (reference.isImplicit && !reference.isInline) {
let hasAnOtherImplicitRef = false;
for (const referenceToOther of referenceToOthersSet) {
if (
referenceToOther.isImplicit &&
referenceToOther.url === reference.url
) {
hasAnOtherImplicitRef = true;
break;
}
}
if (!hasAnOtherImplicitRef) {
ownerUrlInfo.implicitUrlSet.delete(reference.url);
}
}
const prevReference = reference.prev;
const nextReference = reference.next;
if (prevReference && nextReference) {
nextReference.prev = prevReference;
prevReference.next = nextReference;
} else if (prevReference) {
prevReference.next = null;
} else if (nextReference) {
nextReference.original = null;
nextReference.prev = null;
}
const referencedUrlInfo = reference.urlInfo;
referencedUrlInfo.referenceFromOthersSet.delete(reference);
let firstReferenceFromOther;
let wasInlined;
for (const referenceFromOther of referencedUrlInfo.referenceFromOthersSet) {
if (referenceFromOther.urlInfo !== referencedUrlInfo) {
continue;
}
// Here we want to know if the file is referenced by an other file.
// So we want to ignore reference that are created by other means:
// - "http_request"
// This type of reference is created when client request a file
// that we don't know yet
// 1. reference(s) to this file are not yet discovered
// 2. there is no reference to this file
if (referenceFromOther.type === "http_request") {
continue;
}
wasInlined = referenceFromOther.gotInlined();
if (wasInlined) {
// the url info was inlined, an other reference is required
// to consider the non-inlined urlInfo as used
continue;
}
firstReferenceFromOther = referenceFromOther;
break;
}
if (firstReferenceFromOther) {
// either applying new ref should override old ref
// or we should first remove effects before adding new ones
// for now we just set firstReference to null
if (reference === referencedUrlInfo.firstReference) {
referencedUrlInfo.firstReference = null;
applyReferenceEffectsOnUrlInfo(firstReferenceFromOther);
}
return false;
}
if (wasInlined) {
return false;
}
// referencedUrlInfo.firstReference = null;
// referencedUrlInfo.lastReference = null;
referencedUrlInfo.onDereferenced(reference);
return true;
};
const traceFromUrlSite = (urlSite) => {
const codeFrame = urlSite.content
? generateContentFrame({
content: urlSite.content,
line: urlSite.line,
column: urlSite.column,
})
: "";
return {
codeFrame,
message: stringifyUrlSite(urlSite),
url: urlSite.url,
line: urlSite.line,
column: urlSite.column,
};
};
const adjustUrlSite = (urlInfo, { url, line, column }) => {
const isOriginal = url === urlInfo.url;
const adjust = (urlInfo, urlSite) => {
if (!urlSite.isOriginal) {
return urlSite;
}
const inlineUrlSite = urlInfo.inlineUrlSite;
if (!inlineUrlSite) {
return urlSite;
}
const parentUrlInfo = urlInfo.graph.getUrlInfo(inlineUrlSite.url);
line =
inlineUrlSite.line === undefined
? urlSite.line
: inlineUrlSite.line + urlSite.line;
// we remove 1 to the line because imagine the following html:
// <style>body { color: red; }</style>
// -> content starts same line as <style> (same for <script>)
if (urlInfo.content[0] === "\n") {
line = line - 1;
}
column =
inlineUrlSite.column === undefined
? urlSite.column
: inlineUrlSite.column + urlSite.column;
return adjust(parentUrlInfo, {
isOriginal: true,
url: inlineUrlSite.url,
content: inlineUrlSite.content,
line,
column,
});
};
return adjust(urlInfo, {
isOriginal,
url,
content: isOriginal ? urlInfo.originalContent : urlInfo.content,
line,
column,
});
};
const getRedirectedReferenceProps = (reference, url) => {
const redirectedProps = {
...reference,
specifier: url,
url,
original: reference.original || reference,
prev: reference,
};
return redirectedProps;
};
const applyReferenceEffectsOnUrlInfo = (reference) => {
const referencedUrlInfo = reference.urlInfo;
referencedUrlInfo.lastReference = reference;
if (reference.isInline) {
referencedUrlInfo.isInline = true;
referencedUrlInfo.inlineUrlSite = {
url: reference.ownerUrlInfo.url,
content: reference.isOriginalPosition
? reference.ownerUrlInfo.originalContent
: reference.ownerUrlInfo.content,
line: reference.specifierLine,
column: reference.specifierColumn,
};
}
if (
referencedUrlInfo.firstReference &&
!referencedUrlInfo.firstReference.isWeak
) {
return;
}
referencedUrlInfo.firstReference = reference;
referencedUrlInfo.originalUrl =
referencedUrlInfo.originalUrl || (reference.original || reference).url;
if (reference.isEntryPoint) {
referencedUrlInfo.isEntryPoint = true;
}
if (reference.isDynamicEntryPoint) {
referencedUrlInfo.isDynamicEntryPoint = true;
}
Object.assign(referencedUrlInfo.data, reference.data);
Object.assign(referencedUrlInfo.timing, reference.timing);
if (reference.injected) {
referencedUrlInfo.injected = true;
}
if (reference.filenameHint && !referencedUrlInfo.filenameHint) {
referencedUrlInfo.filenameHint = reference.filenameHint;
}
if (reference.dirnameHint && !referencedUrlInfo.dirnameHint) {
referencedUrlInfo.dirnameHint = reference.dirnameHint;
}
if (reference.debug) {
referencedUrlInfo.debug = true;
}
if (reference.expectedType) {
referencedUrlInfo.typeHint = reference.expectedType;
}
if (reference.expectedSubtype) {
referencedUrlInfo.subtypeHint = reference.expectedSubtype;
}
referencedUrlInfo.entryUrlInfo = reference.isEntryPoint
? referencedUrlInfo
: reference.ownerUrlInfo.entryUrlInfo;
for (const urlInfoEffect of reference.urlInfoEffectSet) {
urlInfoEffect(referencedUrlInfo);
}
};