UNPKG

@jsenv/core

Version:

Tool to develop, test and build js projects

766 lines (727 loc) • 21.9 kB
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); } };