UNPKG

@jsenv/plugin-transpilation

Version:
269 lines (255 loc) 8.69 kB
/* * Jsenv wont touch code where "specifier" or "type" is dynamic (see code below) * ```js * const file = "./style.css" * const type = "css" * import(file, { with: { type }}) * ``` * Jsenv could throw an error when it knows some browsers in runtimeCompat * do not support import attributes * But for now (as it is simpler) we let the browser throw the error */ import { injectQueryParams, urlToFilename } from "@jsenv/urls"; import { JS_QUOTES } from "@jsenv/utils/src/string/js_quotes.js"; export const jsenvPluginImportAttributes = ({ json = "auto", css = true, text = "auto", }) => { const transpilations = { json, css, text }; const markAsJsModuleProxy = (reference) => { reference.expectedType = "js_module"; if (!reference.filenameHint) { reference.filenameHint = `${urlToFilename(reference.url)}.js`; } }; const turnIntoJsModuleProxy = ( reference, type, { injectSearchParamForSideEffectImports }, ) => { reference.mutation = (magicSource) => { if (reference.subtype === "import_dynamic") { const { importTypeAttributeNode } = reference.astInfo; magicSource.remove({ start: importTypeAttributeNode.start, end: importTypeAttributeNode.end, }); } else { const { importTypeAttributeNode } = reference.astInfo; const content = reference.ownerUrlInfo.content; const withKeywordStart = content.indexOf( "with", importTypeAttributeNode.start - " with { ".length, ); const withKeywordEnd = content.indexOf( "}", importTypeAttributeNode.end, ); magicSource.remove({ start: withKeywordStart, end: withKeywordEnd + 1, }); } }; const newUrl = injectQueryParams(reference.url, { [`as_${type}_module`]: "", ...(injectSearchParamForSideEffectImports && reference.isSideEffectImport ? { side_effect: "" } : {}), }); markAsJsModuleProxy(reference, type); return newUrl; }; const createImportTypePlugin = ({ type, createUrlContent, injectSearchParamForSideEffectImports, }) => { return { name: `jsenv:import_type_${type}`, appliesDuring: "*", init: (context) => { // transpilation is forced during build so that // - avoid rollup to see import assertions // We would have to tell rollup to ignore import with assertion // - means rollup can bundle more js file together // - means url versioning can work for css inlined in js if (context.build) { return true; } const transpilation = transpilations[type]; if (transpilation === "auto") { return !context.isSupportedOnCurrentClients(`import_type_${type}`); } return transpilation; }, redirectReference: (reference) => { if (!reference.importAttributes) { return null; } const { searchParams } = reference; if (searchParams.has(`as_${type}_module`)) { markAsJsModuleProxy(reference, type); return null; } // when search param is injected, it will be removed later // by "getWithoutSearchParam". We don't want to redirect again // (would create infinite recursion) if ( reference.prev && reference.prev.searchParams.has(`as_${type}_module`) ) { return null; } if (reference.importAttributes.type === type) { return turnIntoJsModuleProxy(reference, type, { injectSearchParamForSideEffectImports, }); } return null; }, fetchUrlContent: async (urlInfo) => { const originalUrlInfo = urlInfo.getWithoutSearchParam( `as_${type}_module`, { expectedType: type, }, ); if (!originalUrlInfo) { return null; } await originalUrlInfo.cook(); if ( injectSearchParamForSideEffectImports && originalUrlInfo.searchParams.has("side_effect") ) { const urlInfoWithoutSideEffect = originalUrlInfo.getWithoutSearchParam("side_effect"); if (urlInfoWithoutSideEffect) { await urlInfoWithoutSideEffect.kitchen.urlInfoTransformer.setContent( urlInfoWithoutSideEffect, originalUrlInfo.content, ); } } return createUrlContent(originalUrlInfo); }, }; }; const asJsonModule = createImportTypePlugin({ type: "json", createUrlContent: (jsonUrlInfo) => { const jsonText = JSON.stringify(jsonUrlInfo.content.trim()); let inlineContentCall; // here we could `export default ${jsonText}`: // but js engine are optimized to recognize JSON.parse // and use a faster parsing strategy if (jsonUrlInfo.context.dev) { inlineContentCall = `JSON.parse( ${jsonText}, //# inlinedFromUrl=${jsonUrlInfo.url} )`; } else { inlineContentCall = `JSON.parse(${jsonText})`; } return { content: `export default ${inlineContentCall};`, contentType: "text/javascript", type: "js_module", originalUrl: jsonUrlInfo.originalUrl, originalContent: jsonUrlInfo.originalContent, data: jsonUrlInfo.data, }; }, }); const asCssModule = createImportTypePlugin({ type: "css", injectSearchParamForSideEffectImports: true, createUrlContent: (cssUrlInfo) => { const cssText = JS_QUOTES.escapeSpecialChars(cssUrlInfo.content, { // If template string is choosen and runtime do not support template literals // it's ok because "jsenv:new_inline_content" plugin executes after this one // and convert template strings into raw strings canUseTemplateString: true, }); let inlineContentCall; if (cssUrlInfo.context.dev) { inlineContentCall = `new __InlineContent__( ${cssText}, { type: "text/css" }, //# inlinedFromUrl=${cssUrlInfo.url} )`; } else { inlineContentCall = `new __InlineContent__(${cssText}, { type: "text/css" })`; } let autoInject = cssUrlInfo.searchParams.has("side_effect"); let cssModuleAutoInjectCode = ``; if (autoInject) { if (cssUrlInfo.context.dev) { cssModuleAutoInjectCode = ` document.adoptedStyleSheets = [...document.adoptedStyleSheets, stylesheet]; if (import.meta.hot) { import.meta.hot.dispose(() => { document.adoptedStyleSheets = document.adoptedStyleSheets.filter( (s) => s !== stylesheet, ); }); }; `; } else { cssModuleAutoInjectCode = ` document.adoptedStyleSheets = [...document.adoptedStyleSheets, stylesheet]; `; } } let cssModuleContent = `import ${JSON.stringify(cssUrlInfo.context.inlineContentClientFileUrl)}; const inlineContent = ${inlineContentCall}; const stylesheet = new CSSStyleSheet(); stylesheet.replaceSync(inlineContent.text); ${cssModuleAutoInjectCode} export default stylesheet;`; return { content: cssModuleContent, contentType: "text/javascript", type: "js_module", originalUrl: cssUrlInfo.originalUrl, originalContent: cssUrlInfo.originalContent, data: cssUrlInfo.data, }; }, }); const asTextModule = createImportTypePlugin({ type: "text", createUrlContent: (textUrlInfo) => { const textPlain = JS_QUOTES.escapeSpecialChars(textUrlInfo.content, { // If template string is choosen and runtime do not support template literals // it's ok because "jsenv:new_inline_content" plugin executes after this one // and convert template strings into raw strings canUseTemplateString: true, }); let inlineContentCall; if (textUrlInfo.context.dev) { inlineContentCall = `new __InlineContent__( ${textPlain}, { type: "text/plain"}, //# inlinedFromUrl=${textUrlInfo.url} )`; } else { inlineContentCall = `new __InlineContent__(${textPlain}, { type: "text/plain"})`; } return { content: ` import ${JSON.stringify(textUrlInfo.context.inlineContentClientFileUrl)}; const inlineContent = ${inlineContentCall}; export default inlineContent.text;`, contentType: "text/javascript", type: "js_module", originalUrl: textUrlInfo.originalUrl, originalContent: textUrlInfo.originalContent, data: textUrlInfo.data, }; }, }); return [asJsonModule, asCssModule, asTextModule]; };