diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 00000000..d35c6688 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,146 @@ +# Code of Conduct + +## Our Pledge + +Welcome to our community! We are committed to creating a welcoming and inclusive +environment for all contributors. As members, contributors, and leaders, we +pledge to make participation in our community a harassment-free experience for +everyone, regardless of: + +- Age +- Body size +- Visible or invisible disability +- Ethnicity +- Sex characteristics +- Gender identity and expression +- Level of experience +- Education +- Socio-economic status +- Nationality +- Personal appearance +- Race +- Caste +- Color +- Religion +- Sexual identity and orientation + +We promise to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +- Demonstrating empathy and kindness toward other people +- Being respectful of differing opinions, viewpoints, and experiences +- Giving and gracefully accepting constructive feedback +- Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +- Focusing on what is best not just for us as individuals but for the overall + community + +Examples of unacceptable behavior include: + +- The use of sexualized language or imagery, and sexual attention or advances + of any kind +- Trolling, insulting, or derogatory comments, and personal or political + attacks +- Public or private harassment +- Publishing others' private information, such as a physical or email address, + without their explicit permission +- Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior. They will take appropriate and fair corrective action in +response to any behavior they deem inappropriate, threatening, offensive, or +harmful. This may include removing, editing, or rejecting comments, commits, +code, wiki edits, issues, and other contributions that do not align with this +Code of Conduct. Community leaders will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official e-mail address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at +Community@PlayForm.Cloud. All complaints will be reviewed and investigated +promptly and fairly. All community leaders are obligated to respect the privacy +and security of the reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series of +actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or permanent +ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within the +community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.1, available at +[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. +Community Impact Guidelines were inspired by [Mozilla's code of conduct +enforcement ladder][Mozilla CoC]. + +For answers to common questions about this code of conduct, see the FAQ at +[https://www.contributor-covenant.org/faq][FAQ]. Translations are available at +[https://www.contributor-covenant.org/translations][translations]. + +[homepage]: https://www.contributor-covenant.org +[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html +[Mozilla CoC]: https://github.com/mozilla/diversity +[FAQ]: https://www.contributor-covenant.org/faq +[translations]: https://www.contributor-covenant.org/translations + +Thank you for being part of our community and helping us create a safe and +respectful environment for everyone! diff --git a/Documentation/.nojekyll b/Documentation/.nojekyll new file mode 100644 index 00000000..e2ac6616 --- /dev/null +++ b/Documentation/.nojekyll @@ -0,0 +1 @@ +TypeDoc added this file to prevent GitHub Pages from using Jekyll. You can turn off this behavior by setting the `githubPages` option to false. \ No newline at end of file diff --git a/Documentation/assets/custom.css b/Documentation/assets/custom.css new file mode 100644 index 00000000..e360ec98 --- /dev/null +++ b/Documentation/assets/custom.css @@ -0,0 +1,54 @@ +:root { + --dark-color-background: #000; + --dark-color-background-secondary: #000; + --dark-code-background: #040404; + --color-accent: #2463eb; + --light-hl-0: #b58900; + --light-hl-1: #d33682; + --light-hl-2: #dc322f; + --light-hl-3: #2aa198; + --light-hl-4: #859900; + --dark-hl-0: #ffdd00; + --dark-hl-1: #ff66ff; + --dark-hl-2: #ff4444; + --dark-hl-3: #44ffff; + --dark-hl-4: #44ff44; +} + +body #tsd-search .field label { + left: 50%; + margin-left: -20px; + z-index: 1; + text-align: center; +} + +body #tsd-search.has-focus .field label { + display: none; +} + +body #tsd-search .field input { + z-index: 2; +} + +body pre, +body .tsd-page-toolbar, +body .tsd-generator { + border: none; +} + +body .tsd-navigation a, +body .tsd-navigation summary > span, +body .tsd-page-navigation a { + padding: 0.5rem; + border-radius: 8px; +} + +body .tsd-description .tsd-signatures .tsd-signature, +body .tsd-signature, +body .tsd-signatures .tsd-signature, +body .tsd-typography td, +body .tsd-typography th, +body code.tsd-tag { + border-radius: 12px; + border-width: 2px; +} diff --git a/Documentation/assets/highlight.css b/Documentation/assets/highlight.css new file mode 100644 index 00000000..e2c460e2 --- /dev/null +++ b/Documentation/assets/highlight.css @@ -0,0 +1,85 @@ +:root { + --light-hl-0: #000000; + --dark-hl-0: #D4D4D4; + --light-hl-1: #A31515; + --dark-hl-1: #CE9178; + --light-hl-2: #0000FF; + --dark-hl-2: #569CD6; + --light-hl-3: #0070C1; + --dark-hl-3: #4FC1FF; + --light-hl-4: #795E26; + --dark-hl-4: #DCDCAA; + --light-hl-5: #001080; + --dark-hl-5: #9CDCFE; + --light-hl-6: #098658; + --dark-hl-6: #B5CEA8; + --light-hl-7: #000000; + --dark-hl-7: #C8C8C8; + --light-hl-8: #AF00DB; + --dark-hl-8: #C586C0; + --light-code-background: #FFFFFF; + --dark-code-background: #1E1E1E; +} + +@media (prefers-color-scheme: light) { :root { + --hl-0: var(--light-hl-0); + --hl-1: var(--light-hl-1); + --hl-2: var(--light-hl-2); + --hl-3: var(--light-hl-3); + --hl-4: var(--light-hl-4); + --hl-5: var(--light-hl-5); + --hl-6: var(--light-hl-6); + --hl-7: var(--light-hl-7); + --hl-8: var(--light-hl-8); + --code-background: var(--light-code-background); +} } + +@media (prefers-color-scheme: dark) { :root { + --hl-0: var(--dark-hl-0); + --hl-1: var(--dark-hl-1); + --hl-2: var(--dark-hl-2); + --hl-3: var(--dark-hl-3); + --hl-4: var(--dark-hl-4); + --hl-5: var(--dark-hl-5); + --hl-6: var(--dark-hl-6); + --hl-7: var(--dark-hl-7); + --hl-8: var(--dark-hl-8); + --code-background: var(--dark-code-background); +} } + +:root[data-theme='light'] { + --hl-0: var(--light-hl-0); + --hl-1: var(--light-hl-1); + --hl-2: var(--light-hl-2); + --hl-3: var(--light-hl-3); + --hl-4: var(--light-hl-4); + --hl-5: var(--light-hl-5); + --hl-6: var(--light-hl-6); + --hl-7: var(--light-hl-7); + --hl-8: var(--light-hl-8); + --code-background: var(--light-code-background); +} + +:root[data-theme='dark'] { + --hl-0: var(--dark-hl-0); + --hl-1: var(--dark-hl-1); + --hl-2: var(--dark-hl-2); + --hl-3: var(--dark-hl-3); + --hl-4: var(--dark-hl-4); + --hl-5: var(--dark-hl-5); + --hl-6: var(--dark-hl-6); + --hl-7: var(--dark-hl-7); + --hl-8: var(--dark-hl-8); + --code-background: var(--dark-code-background); +} + +.hl-0 { color: var(--hl-0); } +.hl-1 { color: var(--hl-1); } +.hl-2 { color: var(--hl-2); } +.hl-3 { color: var(--hl-3); } +.hl-4 { color: var(--hl-4); } +.hl-5 { color: var(--hl-5); } +.hl-6 { color: var(--hl-6); } +.hl-7 { color: var(--hl-7); } +.hl-8 { color: var(--hl-8); } +pre, code { background: var(--code-background); } diff --git a/Documentation/assets/icons.js b/Documentation/assets/icons.js new file mode 100644 index 00000000..e88e8ca7 --- /dev/null +++ b/Documentation/assets/icons.js @@ -0,0 +1,18 @@ +(function() { + addIcons(); + function addIcons() { + if (document.readyState === "loading") return document.addEventListener("DOMContentLoaded", addIcons); + const svg = document.body.appendChild(document.createElementNS("http://www.w3.org/2000/svg", "svg")); + svg.innerHTML = `""`; + svg.style.display = "none"; + if (location.protocol === "file:") updateUseElements(); + } + + function updateUseElements() { + document.querySelectorAll("use").forEach(el => { + if (el.getAttribute("href").includes("#icon-")) { + el.setAttribute("href", el.getAttribute("href").replace(/.*#/, "#")); + } + }); + } +})() \ No newline at end of file diff --git a/Documentation/assets/icons.svg b/Documentation/assets/icons.svg new file mode 100644 index 00000000..e371b8b5 --- /dev/null +++ b/Documentation/assets/icons.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Documentation/assets/main.js b/Documentation/assets/main.js new file mode 100644 index 00000000..21a5d74d --- /dev/null +++ b/Documentation/assets/main.js @@ -0,0 +1,60 @@ +"use strict"; +window.translations={"copy":"Copy","copied":"Copied!","normally_hidden":"This member is normally hidden due to your filter settings."}; +"use strict";(()=>{var Pe=Object.create;var ie=Object.defineProperty;var Oe=Object.getOwnPropertyDescriptor;var _e=Object.getOwnPropertyNames;var Re=Object.getPrototypeOf,Me=Object.prototype.hasOwnProperty;var Fe=(t,e)=>()=>(e||t((e={exports:{}}).exports,e),e.exports);var De=(t,e,n,r)=>{if(e&&typeof e=="object"||typeof e=="function")for(let i of _e(e))!Me.call(t,i)&&i!==n&&ie(t,i,{get:()=>e[i],enumerable:!(r=Oe(e,i))||r.enumerable});return t};var Ae=(t,e,n)=>(n=t!=null?Pe(Re(t)):{},De(e||!t||!t.__esModule?ie(n,"default",{value:t,enumerable:!0}):n,t));var ue=Fe((ae,le)=>{(function(){var t=function(e){var n=new t.Builder;return n.pipeline.add(t.trimmer,t.stopWordFilter,t.stemmer),n.searchPipeline.add(t.stemmer),e.call(n,n),n.build()};t.version="2.3.9";t.utils={},t.utils.warn=function(e){return function(n){e.console&&console.warn&&console.warn(n)}}(this),t.utils.asString=function(e){return e==null?"":e.toString()},t.utils.clone=function(e){if(e==null)return e;for(var n=Object.create(null),r=Object.keys(e),i=0;i0){var d=t.utils.clone(n)||{};d.position=[a,u],d.index=s.length,s.push(new t.Token(r.slice(a,o),d))}a=o+1}}return s},t.tokenizer.separator=/[\s\-]+/;t.Pipeline=function(){this._stack=[]},t.Pipeline.registeredFunctions=Object.create(null),t.Pipeline.registerFunction=function(e,n){n in this.registeredFunctions&&t.utils.warn("Overwriting existing registered function: "+n),e.label=n,t.Pipeline.registeredFunctions[e.label]=e},t.Pipeline.warnIfFunctionNotRegistered=function(e){var n=e.label&&e.label in this.registeredFunctions;n||t.utils.warn(`Function is not registered with pipeline. This may cause problems when serialising the index. +`,e)},t.Pipeline.load=function(e){var n=new t.Pipeline;return e.forEach(function(r){var i=t.Pipeline.registeredFunctions[r];if(i)n.add(i);else throw new Error("Cannot load unregistered function: "+r)}),n},t.Pipeline.prototype.add=function(){var e=Array.prototype.slice.call(arguments);e.forEach(function(n){t.Pipeline.warnIfFunctionNotRegistered(n),this._stack.push(n)},this)},t.Pipeline.prototype.after=function(e,n){t.Pipeline.warnIfFunctionNotRegistered(n);var r=this._stack.indexOf(e);if(r==-1)throw new Error("Cannot find existingFn");r=r+1,this._stack.splice(r,0,n)},t.Pipeline.prototype.before=function(e,n){t.Pipeline.warnIfFunctionNotRegistered(n);var r=this._stack.indexOf(e);if(r==-1)throw new Error("Cannot find existingFn");this._stack.splice(r,0,n)},t.Pipeline.prototype.remove=function(e){var n=this._stack.indexOf(e);n!=-1&&this._stack.splice(n,1)},t.Pipeline.prototype.run=function(e){for(var n=this._stack.length,r=0;r1&&(oe&&(r=s),o!=e);)i=r-n,s=n+Math.floor(i/2),o=this.elements[s*2];if(o==e||o>e)return s*2;if(ol?d+=2:a==l&&(n+=r[u+1]*i[d+1],u+=2,d+=2);return n},t.Vector.prototype.similarity=function(e){return this.dot(e)/this.magnitude()||0},t.Vector.prototype.toArray=function(){for(var e=new Array(this.elements.length/2),n=1,r=0;n0){var o=s.str.charAt(0),a;o in s.node.edges?a=s.node.edges[o]:(a=new t.TokenSet,s.node.edges[o]=a),s.str.length==1&&(a.final=!0),i.push({node:a,editsRemaining:s.editsRemaining,str:s.str.slice(1)})}if(s.editsRemaining!=0){if("*"in s.node.edges)var l=s.node.edges["*"];else{var l=new t.TokenSet;s.node.edges["*"]=l}if(s.str.length==0&&(l.final=!0),i.push({node:l,editsRemaining:s.editsRemaining-1,str:s.str}),s.str.length>1&&i.push({node:s.node,editsRemaining:s.editsRemaining-1,str:s.str.slice(1)}),s.str.length==1&&(s.node.final=!0),s.str.length>=1){if("*"in s.node.edges)var u=s.node.edges["*"];else{var u=new t.TokenSet;s.node.edges["*"]=u}s.str.length==1&&(u.final=!0),i.push({node:u,editsRemaining:s.editsRemaining-1,str:s.str.slice(1)})}if(s.str.length>1){var d=s.str.charAt(0),m=s.str.charAt(1),p;m in s.node.edges?p=s.node.edges[m]:(p=new t.TokenSet,s.node.edges[m]=p),s.str.length==1&&(p.final=!0),i.push({node:p,editsRemaining:s.editsRemaining-1,str:d+s.str.slice(2)})}}}return r},t.TokenSet.fromString=function(e){for(var n=new t.TokenSet,r=n,i=0,s=e.length;i=e;n--){var r=this.uncheckedNodes[n],i=r.child.toString();i in this.minimizedNodes?r.parent.edges[r.char]=this.minimizedNodes[i]:(r.child._str=i,this.minimizedNodes[i]=r.child),this.uncheckedNodes.pop()}};t.Index=function(e){this.invertedIndex=e.invertedIndex,this.fieldVectors=e.fieldVectors,this.tokenSet=e.tokenSet,this.fields=e.fields,this.pipeline=e.pipeline},t.Index.prototype.search=function(e){return this.query(function(n){var r=new t.QueryParser(e,n);r.parse()})},t.Index.prototype.query=function(e){for(var n=new t.Query(this.fields),r=Object.create(null),i=Object.create(null),s=Object.create(null),o=Object.create(null),a=Object.create(null),l=0;l1?this._b=1:this._b=e},t.Builder.prototype.k1=function(e){this._k1=e},t.Builder.prototype.add=function(e,n){var r=e[this._ref],i=Object.keys(this._fields);this._documents[r]=n||{},this.documentCount+=1;for(var s=0;s=this.length)return t.QueryLexer.EOS;var e=this.str.charAt(this.pos);return this.pos+=1,e},t.QueryLexer.prototype.width=function(){return this.pos-this.start},t.QueryLexer.prototype.ignore=function(){this.start==this.pos&&(this.pos+=1),this.start=this.pos},t.QueryLexer.prototype.backup=function(){this.pos-=1},t.QueryLexer.prototype.acceptDigitRun=function(){var e,n;do e=this.next(),n=e.charCodeAt(0);while(n>47&&n<58);e!=t.QueryLexer.EOS&&this.backup()},t.QueryLexer.prototype.more=function(){return this.pos1&&(e.backup(),e.emit(t.QueryLexer.TERM)),e.ignore(),e.more())return t.QueryLexer.lexText},t.QueryLexer.lexEditDistance=function(e){return e.ignore(),e.acceptDigitRun(),e.emit(t.QueryLexer.EDIT_DISTANCE),t.QueryLexer.lexText},t.QueryLexer.lexBoost=function(e){return e.ignore(),e.acceptDigitRun(),e.emit(t.QueryLexer.BOOST),t.QueryLexer.lexText},t.QueryLexer.lexEOS=function(e){e.width()>0&&e.emit(t.QueryLexer.TERM)},t.QueryLexer.termSeparator=t.tokenizer.separator,t.QueryLexer.lexText=function(e){for(;;){var n=e.next();if(n==t.QueryLexer.EOS)return t.QueryLexer.lexEOS;if(n.charCodeAt(0)==92){e.escapeCharacter();continue}if(n==":")return t.QueryLexer.lexField;if(n=="~")return e.backup(),e.width()>0&&e.emit(t.QueryLexer.TERM),t.QueryLexer.lexEditDistance;if(n=="^")return e.backup(),e.width()>0&&e.emit(t.QueryLexer.TERM),t.QueryLexer.lexBoost;if(n=="+"&&e.width()===1||n=="-"&&e.width()===1)return e.emit(t.QueryLexer.PRESENCE),t.QueryLexer.lexText;if(n.match(t.QueryLexer.termSeparator))return t.QueryLexer.lexTerm}},t.QueryParser=function(e,n){this.lexer=new t.QueryLexer(e),this.query=n,this.currentClause={},this.lexemeIdx=0},t.QueryParser.prototype.parse=function(){this.lexer.run(),this.lexemes=this.lexer.lexemes;for(var e=t.QueryParser.parseClause;e;)e=e(this);return this.query},t.QueryParser.prototype.peekLexeme=function(){return this.lexemes[this.lexemeIdx]},t.QueryParser.prototype.consumeLexeme=function(){var e=this.peekLexeme();return this.lexemeIdx+=1,e},t.QueryParser.prototype.nextClause=function(){var e=this.currentClause;this.query.clause(e),this.currentClause={}},t.QueryParser.parseClause=function(e){var n=e.peekLexeme();if(n!=null)switch(n.type){case t.QueryLexer.PRESENCE:return t.QueryParser.parsePresence;case t.QueryLexer.FIELD:return t.QueryParser.parseField;case t.QueryLexer.TERM:return t.QueryParser.parseTerm;default:var r="expected either a field or a term, found "+n.type;throw n.str.length>=1&&(r+=" with value '"+n.str+"'"),new t.QueryParseError(r,n.start,n.end)}},t.QueryParser.parsePresence=function(e){var n=e.consumeLexeme();if(n!=null){switch(n.str){case"-":e.currentClause.presence=t.Query.presence.PROHIBITED;break;case"+":e.currentClause.presence=t.Query.presence.REQUIRED;break;default:var r="unrecognised presence operator'"+n.str+"'";throw new t.QueryParseError(r,n.start,n.end)}var i=e.peekLexeme();if(i==null){var r="expecting term or field, found nothing";throw new t.QueryParseError(r,n.start,n.end)}switch(i.type){case t.QueryLexer.FIELD:return t.QueryParser.parseField;case t.QueryLexer.TERM:return t.QueryParser.parseTerm;default:var r="expecting term or field, found '"+i.type+"'";throw new t.QueryParseError(r,i.start,i.end)}}},t.QueryParser.parseField=function(e){var n=e.consumeLexeme();if(n!=null){if(e.query.allFields.indexOf(n.str)==-1){var r=e.query.allFields.map(function(o){return"'"+o+"'"}).join(", "),i="unrecognised field '"+n.str+"', possible fields: "+r;throw new t.QueryParseError(i,n.start,n.end)}e.currentClause.fields=[n.str];var s=e.peekLexeme();if(s==null){var i="expecting term, found nothing";throw new t.QueryParseError(i,n.start,n.end)}switch(s.type){case t.QueryLexer.TERM:return t.QueryParser.parseTerm;default:var i="expecting term, found '"+s.type+"'";throw new t.QueryParseError(i,s.start,s.end)}}},t.QueryParser.parseTerm=function(e){var n=e.consumeLexeme();if(n!=null){e.currentClause.term=n.str.toLowerCase(),n.str.indexOf("*")!=-1&&(e.currentClause.usePipeline=!1);var r=e.peekLexeme();if(r==null){e.nextClause();return}switch(r.type){case t.QueryLexer.TERM:return e.nextClause(),t.QueryParser.parseTerm;case t.QueryLexer.FIELD:return e.nextClause(),t.QueryParser.parseField;case t.QueryLexer.EDIT_DISTANCE:return t.QueryParser.parseEditDistance;case t.QueryLexer.BOOST:return t.QueryParser.parseBoost;case t.QueryLexer.PRESENCE:return e.nextClause(),t.QueryParser.parsePresence;default:var i="Unexpected lexeme type '"+r.type+"'";throw new t.QueryParseError(i,r.start,r.end)}}},t.QueryParser.parseEditDistance=function(e){var n=e.consumeLexeme();if(n!=null){var r=parseInt(n.str,10);if(isNaN(r)){var i="edit distance must be numeric";throw new t.QueryParseError(i,n.start,n.end)}e.currentClause.editDistance=r;var s=e.peekLexeme();if(s==null){e.nextClause();return}switch(s.type){case t.QueryLexer.TERM:return e.nextClause(),t.QueryParser.parseTerm;case t.QueryLexer.FIELD:return e.nextClause(),t.QueryParser.parseField;case t.QueryLexer.EDIT_DISTANCE:return t.QueryParser.parseEditDistance;case t.QueryLexer.BOOST:return t.QueryParser.parseBoost;case t.QueryLexer.PRESENCE:return e.nextClause(),t.QueryParser.parsePresence;default:var i="Unexpected lexeme type '"+s.type+"'";throw new t.QueryParseError(i,s.start,s.end)}}},t.QueryParser.parseBoost=function(e){var n=e.consumeLexeme();if(n!=null){var r=parseInt(n.str,10);if(isNaN(r)){var i="boost must be numeric";throw new t.QueryParseError(i,n.start,n.end)}e.currentClause.boost=r;var s=e.peekLexeme();if(s==null){e.nextClause();return}switch(s.type){case t.QueryLexer.TERM:return e.nextClause(),t.QueryParser.parseTerm;case t.QueryLexer.FIELD:return e.nextClause(),t.QueryParser.parseField;case t.QueryLexer.EDIT_DISTANCE:return t.QueryParser.parseEditDistance;case t.QueryLexer.BOOST:return t.QueryParser.parseBoost;case t.QueryLexer.PRESENCE:return e.nextClause(),t.QueryParser.parsePresence;default:var i="Unexpected lexeme type '"+s.type+"'";throw new t.QueryParseError(i,s.start,s.end)}}},function(e,n){typeof define=="function"&&define.amd?define(n):typeof ae=="object"?le.exports=n():e.lunr=n()}(this,function(){return t})})()});var se=[];function G(t,e){se.push({selector:e,constructor:t})}var U=class{constructor(){this.alwaysVisibleMember=null;this.createComponents(document.body),this.ensureFocusedElementVisible(),this.listenForCodeCopies(),window.addEventListener("hashchange",()=>this.ensureFocusedElementVisible()),document.body.style.display||(this.ensureFocusedElementVisible(),this.updateIndexVisibility(),this.scrollToHash())}createComponents(e){se.forEach(n=>{e.querySelectorAll(n.selector).forEach(r=>{r.dataset.hasInstance||(new n.constructor({el:r,app:this}),r.dataset.hasInstance=String(!0))})})}filterChanged(){this.ensureFocusedElementVisible()}showPage(){document.body.style.display&&(document.body.style.removeProperty("display"),this.ensureFocusedElementVisible(),this.updateIndexVisibility(),this.scrollToHash())}scrollToHash(){if(location.hash){let e=document.getElementById(location.hash.substring(1));if(!e)return;e.scrollIntoView({behavior:"instant",block:"start"})}}ensureActivePageVisible(){let e=document.querySelector(".tsd-navigation .current"),n=e?.parentElement;for(;n&&!n.classList.contains(".tsd-navigation");)n instanceof HTMLDetailsElement&&(n.open=!0),n=n.parentElement;if(e&&!Ve(e)){let r=e.getBoundingClientRect().top-document.documentElement.clientHeight/4;document.querySelector(".site-menu").scrollTop=r,document.querySelector(".col-sidebar").scrollTop=r}}updateIndexVisibility(){let e=document.querySelector(".tsd-index-content"),n=e?.open;e&&(e.open=!0),document.querySelectorAll(".tsd-index-section").forEach(r=>{r.style.display="block";let i=Array.from(r.querySelectorAll(".tsd-index-link")).every(s=>s.offsetParent==null);r.style.display=i?"none":"block"}),e&&(e.open=n)}ensureFocusedElementVisible(){if(this.alwaysVisibleMember&&(this.alwaysVisibleMember.classList.remove("always-visible"),this.alwaysVisibleMember.firstElementChild.remove(),this.alwaysVisibleMember=null),!location.hash)return;let e=document.getElementById(location.hash.substring(1));if(!e)return;let n=e.parentElement;for(;n&&n.tagName!=="SECTION";)n=n.parentElement;if(!n)return;let r=n.offsetParent==null,i=n;for(;i!==document.body;)i instanceof HTMLDetailsElement&&(i.open=!0),i=i.parentElement;if(n.offsetParent==null){this.alwaysVisibleMember=n,n.classList.add("always-visible");let s=document.createElement("p");s.classList.add("warning"),s.textContent=window.translations.normally_hidden,n.prepend(s)}r&&e.scrollIntoView()}listenForCodeCopies(){document.querySelectorAll("pre > button").forEach(e=>{let n;e.addEventListener("click",()=>{e.previousElementSibling instanceof HTMLElement&&navigator.clipboard.writeText(e.previousElementSibling.innerText.trim()),e.textContent=window.translations.copied,e.classList.add("visible"),clearTimeout(n),n=setTimeout(()=>{e.classList.remove("visible"),n=setTimeout(()=>{e.textContent=window.translations.copy},100)},1e3)})})}};function Ve(t){let e=t.getBoundingClientRect(),n=Math.max(document.documentElement.clientHeight,window.innerHeight);return!(e.bottom<0||e.top-n>=0)}var oe=(t,e=100)=>{let n;return()=>{clearTimeout(n),n=setTimeout(()=>t(),e)}};var pe=Ae(ue());async function ce(t,e){if(!window.searchData)return;let n=await fetch(window.searchData),r=new Blob([await n.arrayBuffer()]).stream().pipeThrough(new DecompressionStream("gzip")),i=await new Response(r).json();t.data=i,t.index=pe.Index.load(i.index),e.classList.remove("loading"),e.classList.add("ready")}function fe(){let t=document.getElementById("tsd-search");if(!t)return;let e={base:t.dataset.base+"/"},n=document.getElementById("tsd-search-script");t.classList.add("loading"),n&&(n.addEventListener("error",()=>{t.classList.remove("loading"),t.classList.add("failure")}),n.addEventListener("load",()=>{ce(e,t)}),ce(e,t));let r=document.querySelector("#tsd-search input"),i=document.querySelector("#tsd-search .results");if(!r||!i)throw new Error("The input field or the result list wrapper was not found");i.addEventListener("mouseup",()=>{te(t)}),r.addEventListener("focus",()=>t.classList.add("has-focus")),He(t,i,r,e)}function He(t,e,n,r){n.addEventListener("input",oe(()=>{Ne(t,e,n,r)},200)),n.addEventListener("keydown",i=>{i.key=="Enter"?Be(e,t):i.key=="ArrowUp"?(de(e,n,-1),i.preventDefault()):i.key==="ArrowDown"&&(de(e,n,1),i.preventDefault())}),document.body.addEventListener("keypress",i=>{i.altKey||i.ctrlKey||i.metaKey||!n.matches(":focus")&&i.key==="/"&&(i.preventDefault(),n.focus())}),document.body.addEventListener("keyup",i=>{t.classList.contains("has-focus")&&(i.key==="Escape"||!e.matches(":focus-within")&&!n.matches(":focus"))&&(n.blur(),te(t))})}function te(t){t.classList.remove("has-focus")}function Ne(t,e,n,r){if(!r.index||!r.data)return;e.textContent="";let i=n.value.trim(),s;if(i){let o=i.split(" ").map(a=>a.length?`*${a}*`:"").join(" ");s=r.index.search(o)}else s=[];for(let o=0;oa.score-o.score);for(let o=0,a=Math.min(10,s.length);o`,d=he(l.name,i);globalThis.DEBUG_SEARCH_WEIGHTS&&(d+=` (score: ${s[o].score.toFixed(2)})`),l.parent&&(d=` + ${he(l.parent,i)}.${d}`);let m=document.createElement("li");m.classList.value=l.classes??"";let p=document.createElement("a");p.href=r.base+l.url,p.innerHTML=u+d,m.append(p),p.addEventListener("focus",()=>{e.querySelector(".current")?.classList.remove("current"),m.classList.add("current")}),e.appendChild(m)}}function de(t,e,n){let r=t.querySelector(".current");if(!r)r=t.querySelector(n==1?"li:first-child":"li:last-child"),r&&r.classList.add("current");else{let i=r;if(n===1)do i=i.nextElementSibling??void 0;while(i instanceof HTMLElement&&i.offsetParent==null);else do i=i.previousElementSibling??void 0;while(i instanceof HTMLElement&&i.offsetParent==null);i?(r.classList.remove("current"),i.classList.add("current")):n===-1&&(r.classList.remove("current"),e.focus())}}function Be(t,e){let n=t.querySelector(".current");if(n||(n=t.querySelector("li:first-child")),n){let r=n.querySelector("a");r&&(window.location.href=r.href),te(e)}}function he(t,e){if(e==="")return t;let n=t.toLocaleLowerCase(),r=e.toLocaleLowerCase(),i=[],s=0,o=n.indexOf(r);for(;o!=-1;)i.push(ee(t.substring(s,o)),`${ee(t.substring(o,o+r.length))}`),s=o+r.length,o=n.indexOf(r,s);return i.push(ee(t.substring(s))),i.join("")}var je={"&":"&","<":"<",">":">","'":"'",'"':"""};function ee(t){return t.replace(/[&<>"'"]/g,e=>je[e])}var I=class{constructor(e){this.el=e.el,this.app=e.app}};var F="mousedown",ye="mousemove",N="mouseup",J={x:0,y:0},me=!1,ne=!1,qe=!1,D=!1,ve=/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);document.documentElement.classList.add(ve?"is-mobile":"not-mobile");ve&&"ontouchstart"in document.documentElement&&(qe=!0,F="touchstart",ye="touchmove",N="touchend");document.addEventListener(F,t=>{ne=!0,D=!1;let e=F=="touchstart"?t.targetTouches[0]:t;J.y=e.pageY||0,J.x=e.pageX||0});document.addEventListener(ye,t=>{if(ne&&!D){let e=F=="touchstart"?t.targetTouches[0]:t,n=J.x-(e.pageX||0),r=J.y-(e.pageY||0);D=Math.sqrt(n*n+r*r)>10}});document.addEventListener(N,()=>{ne=!1});document.addEventListener("click",t=>{me&&(t.preventDefault(),t.stopImmediatePropagation(),me=!1)});var X=class extends I{constructor(e){super(e),this.className=this.el.dataset.toggle||"",this.el.addEventListener(N,n=>this.onPointerUp(n)),this.el.addEventListener("click",n=>n.preventDefault()),document.addEventListener(F,n=>this.onDocumentPointerDown(n)),document.addEventListener(N,n=>this.onDocumentPointerUp(n))}setActive(e){if(this.active==e)return;this.active=e,document.documentElement.classList.toggle("has-"+this.className,e),this.el.classList.toggle("active",e);let n=(this.active?"to-has-":"from-has-")+this.className;document.documentElement.classList.add(n),setTimeout(()=>document.documentElement.classList.remove(n),500)}onPointerUp(e){D||(this.setActive(!0),e.preventDefault())}onDocumentPointerDown(e){if(this.active){if(e.target.closest(".col-sidebar, .tsd-filter-group"))return;this.setActive(!1)}}onDocumentPointerUp(e){if(!D&&this.active&&e.target.closest(".col-sidebar")){let n=e.target.closest("a");if(n){let r=window.location.href;r.indexOf("#")!=-1&&(r=r.substring(0,r.indexOf("#"))),n.href.substring(0,r.length)==r&&setTimeout(()=>this.setActive(!1),250)}}}};var re;try{re=localStorage}catch{re={getItem(){return null},setItem(){}}}var Q=re;var ge=document.head.appendChild(document.createElement("style"));ge.dataset.for="filters";var Y=class extends I{constructor(e){super(e),this.key=`filter-${this.el.name}`,this.value=this.el.checked,this.el.addEventListener("change",()=>{this.setLocalStorage(this.el.checked)}),this.setLocalStorage(this.fromLocalStorage()),ge.innerHTML+=`html:not(.${this.key}) .tsd-is-${this.el.name} { display: none; } +`,this.app.updateIndexVisibility()}fromLocalStorage(){let e=Q.getItem(this.key);return e?e==="true":this.el.checked}setLocalStorage(e){Q.setItem(this.key,e.toString()),this.value=e,this.handleValueChange()}handleValueChange(){this.el.checked=this.value,document.documentElement.classList.toggle(this.key,this.value),this.app.filterChanged(),this.app.updateIndexVisibility()}};var Z=class extends I{constructor(e){super(e),this.summary=this.el.querySelector(".tsd-accordion-summary"),this.icon=this.summary.querySelector("svg"),this.key=`tsd-accordion-${this.summary.dataset.key??this.summary.textContent.trim().replace(/\s+/g,"-").toLowerCase()}`;let n=Q.getItem(this.key);this.el.open=n?n==="true":this.el.open,this.el.addEventListener("toggle",()=>this.update());let r=this.summary.querySelector("a");r&&r.addEventListener("click",()=>{location.assign(r.href)}),this.update()}update(){this.icon.style.transform=`rotate(${this.el.open?0:-90}deg)`,Q.setItem(this.key,this.el.open.toString())}};function Ee(t){let e=Q.getItem("tsd-theme")||"os";t.value=e,xe(e),t.addEventListener("change",()=>{Q.setItem("tsd-theme",t.value),xe(t.value)})}function xe(t){document.documentElement.dataset.theme=t}var K;function we(){let t=document.getElementById("tsd-nav-script");t&&(t.addEventListener("load",Le),Le())}async function Le(){let t=document.getElementById("tsd-nav-container");if(!t||!window.navigationData)return;let n=await(await fetch(window.navigationData)).arrayBuffer(),r=new Blob([n]).stream().pipeThrough(new DecompressionStream("gzip")),i=await new Response(r).json();K=t.dataset.base,K.endsWith("/")||(K+="/"),t.innerHTML="";for(let s of i)Se(s,t,[]);window.app.createComponents(t),window.app.showPage(),window.app.ensureActivePageVisible()}function Se(t,e,n){let r=e.appendChild(document.createElement("li"));if(t.children){let i=[...n,t.text],s=r.appendChild(document.createElement("details"));s.className=t.class?`${t.class} tsd-accordion`:"tsd-accordion";let o=s.appendChild(document.createElement("summary"));o.className="tsd-accordion-summary",o.dataset.key=i.join("$"),o.innerHTML='',be(t,o);let a=s.appendChild(document.createElement("div"));a.className="tsd-accordion-details";let l=a.appendChild(document.createElement("ul"));l.className="tsd-nested-navigation";for(let u of t.children)Se(u,l,i)}else be(t,r,t.class)}function be(t,e,n){if(t.path){let r=e.appendChild(document.createElement("a"));r.href=K+t.path,n&&(r.className=n),location.pathname===r.pathname&&!r.href.includes("#")&&r.classList.add("current"),t.kind&&(r.innerHTML=``),r.appendChild(document.createElement("span")).textContent=t.text}else e.appendChild(document.createElement("span")).textContent=t.text}G(X,"a[data-toggle]");G(Z,".tsd-accordion");G(Y,".tsd-filter-item input[type=checkbox]");var Te=document.getElementById("tsd-theme");Te&&Ee(Te);var $e=new U;Object.defineProperty(window,"app",{value:$e});fe();we();})(); +/*! Bundled license information: + +lunr/lunr.js: + (** + * lunr - http://lunrjs.com - A bit like Solr, but much smaller and not as bright - 2.3.9 + * Copyright (C) 2020 Oliver Nightingale + * @license MIT + *) + (*! + * lunr.utils + * Copyright (C) 2020 Oliver Nightingale + *) + (*! + * lunr.Set + * Copyright (C) 2020 Oliver Nightingale + *) + (*! + * lunr.tokenizer + * Copyright (C) 2020 Oliver Nightingale + *) + (*! + * lunr.Pipeline + * Copyright (C) 2020 Oliver Nightingale + *) + (*! + * lunr.Vector + * Copyright (C) 2020 Oliver Nightingale + *) + (*! + * lunr.stemmer + * Copyright (C) 2020 Oliver Nightingale + * Includes code from - http://tartarus.org/~martin/PorterStemmer/js.txt + *) + (*! + * lunr.stopWordFilter + * Copyright (C) 2020 Oliver Nightingale + *) + (*! + * lunr.trimmer + * Copyright (C) 2020 Oliver Nightingale + *) + (*! + * lunr.TokenSet + * Copyright (C) 2020 Oliver Nightingale + *) + (*! + * lunr.Index + * Copyright (C) 2020 Oliver Nightingale + *) + (*! + * lunr.Builder + * Copyright (C) 2020 Oliver Nightingale + *) +*/ diff --git a/Documentation/assets/navigation.js b/Documentation/assets/navigation.js new file mode 100644 index 00000000..e7335e96 --- /dev/null +++ b/Documentation/assets/navigation.js @@ -0,0 +1 @@ +window.navigationData = "data:application/octet-stream;base64,H4sIAAAAAAAACouOBQApu0wNAgAAAA==" \ No newline at end of file diff --git a/Documentation/assets/search.js b/Documentation/assets/search.js new file mode 100644 index 00000000..31a4e1af --- /dev/null +++ b/Documentation/assets/search.js @@ -0,0 +1 @@ +window.searchData = "data:application/octet-stream;base64,H4sIAAAAAAAACj2MQQqDMBRE7zLr4MKuzA16ATfioiQjfDD/S0xtQby7pEp384bH25Hts8IPo4No5Bd+x8a8iik82ubRdHCYhHOsGvSVCIdgKVELHKKF92+Ot9YzFMv/5sZcGJ9Xu16LLJxFWek4TiJTfzuBAAAA"; \ No newline at end of file diff --git a/Documentation/assets/style.css b/Documentation/assets/style.css new file mode 100644 index 00000000..9d619a64 --- /dev/null +++ b/Documentation/assets/style.css @@ -0,0 +1,1448 @@ +:root { + /* Light */ + --light-color-background: #f2f4f8; + --light-color-background-secondary: #eff0f1; + --light-color-warning-text: #222; + --light-color-background-warning: #e6e600; + --light-color-icon-background: var(--light-color-background); + --light-color-accent: #c5c7c9; + --light-color-active-menu-item: var(--light-color-accent); + --light-color-text: #222; + --light-color-text-aside: #6e6e6e; + --light-color-link: #1f70c2; + --light-color-focus-outline: #3584e4; + + --light-color-ts-keyword: #056bd6; + --light-color-ts-project: #b111c9; + --light-color-ts-module: var(--light-color-ts-project); + --light-color-ts-namespace: var(--light-color-ts-project); + --light-color-ts-enum: #7e6f15; + --light-color-ts-enum-member: var(--light-color-ts-enum); + --light-color-ts-variable: #4760ec; + --light-color-ts-function: #572be7; + --light-color-ts-class: #1f70c2; + --light-color-ts-interface: #108024; + --light-color-ts-constructor: var(--light-color-ts-class); + --light-color-ts-property: var(--light-color-ts-variable); + --light-color-ts-method: var(--light-color-ts-function); + --light-color-ts-call-signature: var(--light-color-ts-method); + --light-color-ts-index-signature: var(--light-color-ts-property); + --light-color-ts-constructor-signature: var(--light-color-ts-constructor); + --light-color-ts-parameter: var(--light-color-ts-variable); + /* type literal not included as links will never be generated to it */ + --light-color-ts-type-parameter: #a55c0e; + --light-color-ts-accessor: var(--light-color-ts-property); + --light-color-ts-get-signature: var(--light-color-ts-accessor); + --light-color-ts-set-signature: var(--light-color-ts-accessor); + --light-color-ts-type-alias: #d51270; + /* reference not included as links will be colored with the kind that it points to */ + --light-color-document: #000000; + + --light-external-icon: url("data:image/svg+xml;utf8,"); + --light-color-scheme: light; + + /* Dark */ + --dark-color-background: #2b2e33; + --dark-color-background-secondary: #1e2024; + --dark-color-background-warning: #bebe00; + --dark-color-warning-text: #222; + --dark-color-icon-background: var(--dark-color-background-secondary); + --dark-color-accent: #9096a2; + --dark-color-active-menu-item: #5d5d6a; + --dark-color-text: #f5f5f5; + --dark-color-text-aside: #dddddd; + --dark-color-link: #00aff4; + --dark-color-focus-outline: #4c97f2; + + --dark-color-ts-keyword: #3399ff; + --dark-color-ts-project: #e358ff; + --dark-color-ts-module: var(--dark-color-ts-project); + --dark-color-ts-namespace: var(--dark-color-ts-project); + --dark-color-ts-enum: #f4d93e; + --dark-color-ts-enum-member: var(--dark-color-ts-enum); + --dark-color-ts-variable: #798dff; + --dark-color-ts-function: #a280ff; + --dark-color-ts-class: #8ac4ff; + --dark-color-ts-interface: #6cff87; + --dark-color-ts-constructor: var(--dark-color-ts-class); + --dark-color-ts-property: var(--dark-color-ts-variable); + --dark-color-ts-method: var(--dark-color-ts-function); + --dark-color-ts-call-signature: var(--dark-color-ts-method); + --dark-color-ts-index-signature: var(--dark-color-ts-property); + --dark-color-ts-constructor-signature: var(--dark-color-ts-constructor); + --dark-color-ts-parameter: var(--dark-color-ts-variable); + /* type literal not included as links will never be generated to it */ + --dark-color-ts-type-parameter: #e07d13; + --dark-color-ts-accessor: var(--dark-color-ts-property); + --dark-color-ts-get-signature: var(--dark-color-ts-accessor); + --dark-color-ts-set-signature: var(--dark-color-ts-accessor); + --dark-color-ts-type-alias: #ff6492; + /* reference not included as links will be colored with the kind that it points to */ + --dark-color-document: #ffffff; + + --dark-external-icon: url("data:image/svg+xml;utf8,"); + --dark-color-scheme: dark; +} + +@media (prefers-color-scheme: light) { + :root { + --color-background: var(--light-color-background); + --color-background-secondary: var(--light-color-background-secondary); + --color-background-warning: var(--light-color-background-warning); + --color-warning-text: var(--light-color-warning-text); + --color-icon-background: var(--light-color-icon-background); + --color-accent: var(--light-color-accent); + --color-active-menu-item: var(--light-color-active-menu-item); + --color-text: var(--light-color-text); + --color-text-aside: var(--light-color-text-aside); + --color-link: var(--light-color-link); + --color-focus-outline: var(--light-color-focus-outline); + + --color-ts-keyword: var(--light-color-ts-keyword); + --color-ts-module: var(--light-color-ts-module); + --color-ts-namespace: var(--light-color-ts-namespace); + --color-ts-enum: var(--light-color-ts-enum); + --color-ts-enum-member: var(--light-color-ts-enum-member); + --color-ts-variable: var(--light-color-ts-variable); + --color-ts-function: var(--light-color-ts-function); + --color-ts-class: var(--light-color-ts-class); + --color-ts-interface: var(--light-color-ts-interface); + --color-ts-constructor: var(--light-color-ts-constructor); + --color-ts-property: var(--light-color-ts-property); + --color-ts-method: var(--light-color-ts-method); + --color-ts-call-signature: var(--light-color-ts-call-signature); + --color-ts-index-signature: var(--light-color-ts-index-signature); + --color-ts-constructor-signature: var( + --light-color-ts-constructor-signature + ); + --color-ts-parameter: var(--light-color-ts-parameter); + --color-ts-type-parameter: var(--light-color-ts-type-parameter); + --color-ts-accessor: var(--light-color-ts-accessor); + --color-ts-get-signature: var(--light-color-ts-get-signature); + --color-ts-set-signature: var(--light-color-ts-set-signature); + --color-ts-type-alias: var(--light-color-ts-type-alias); + --color-document: var(--light-color-document); + + --external-icon: var(--light-external-icon); + --color-scheme: var(--light-color-scheme); + } +} + +@media (prefers-color-scheme: dark) { + :root { + --color-background: var(--dark-color-background); + --color-background-secondary: var(--dark-color-background-secondary); + --color-background-warning: var(--dark-color-background-warning); + --color-warning-text: var(--dark-color-warning-text); + --color-icon-background: var(--dark-color-icon-background); + --color-accent: var(--dark-color-accent); + --color-active-menu-item: var(--dark-color-active-menu-item); + --color-text: var(--dark-color-text); + --color-text-aside: var(--dark-color-text-aside); + --color-link: var(--dark-color-link); + --color-focus-outline: var(--dark-color-focus-outline); + + --color-ts-keyword: var(--dark-color-ts-keyword); + --color-ts-module: var(--dark-color-ts-module); + --color-ts-namespace: var(--dark-color-ts-namespace); + --color-ts-enum: var(--dark-color-ts-enum); + --color-ts-enum-member: var(--dark-color-ts-enum-member); + --color-ts-variable: var(--dark-color-ts-variable); + --color-ts-function: var(--dark-color-ts-function); + --color-ts-class: var(--dark-color-ts-class); + --color-ts-interface: var(--dark-color-ts-interface); + --color-ts-constructor: var(--dark-color-ts-constructor); + --color-ts-property: var(--dark-color-ts-property); + --color-ts-method: var(--dark-color-ts-method); + --color-ts-call-signature: var(--dark-color-ts-call-signature); + --color-ts-index-signature: var(--dark-color-ts-index-signature); + --color-ts-constructor-signature: var( + --dark-color-ts-constructor-signature + ); + --color-ts-parameter: var(--dark-color-ts-parameter); + --color-ts-type-parameter: var(--dark-color-ts-type-parameter); + --color-ts-accessor: var(--dark-color-ts-accessor); + --color-ts-get-signature: var(--dark-color-ts-get-signature); + --color-ts-set-signature: var(--dark-color-ts-set-signature); + --color-ts-type-alias: var(--dark-color-ts-type-alias); + --color-document: var(--dark-color-document); + + --external-icon: var(--dark-external-icon); + --color-scheme: var(--dark-color-scheme); + } +} + +html { + color-scheme: var(--color-scheme); +} + +body { + margin: 0; +} + +:root[data-theme="light"] { + --color-background: var(--light-color-background); + --color-background-secondary: var(--light-color-background-secondary); + --color-background-warning: var(--light-color-background-warning); + --color-warning-text: var(--light-color-warning-text); + --color-icon-background: var(--light-color-icon-background); + --color-accent: var(--light-color-accent); + --color-active-menu-item: var(--light-color-active-menu-item); + --color-text: var(--light-color-text); + --color-text-aside: var(--light-color-text-aside); + --color-link: var(--light-color-link); + --color-focus-outline: var(--light-color-focus-outline); + + --color-ts-keyword: var(--light-color-ts-keyword); + --color-ts-module: var(--light-color-ts-module); + --color-ts-namespace: var(--light-color-ts-namespace); + --color-ts-enum: var(--light-color-ts-enum); + --color-ts-enum-member: var(--light-color-ts-enum-member); + --color-ts-variable: var(--light-color-ts-variable); + --color-ts-function: var(--light-color-ts-function); + --color-ts-class: var(--light-color-ts-class); + --color-ts-interface: var(--light-color-ts-interface); + --color-ts-constructor: var(--light-color-ts-constructor); + --color-ts-property: var(--light-color-ts-property); + --color-ts-method: var(--light-color-ts-method); + --color-ts-call-signature: var(--light-color-ts-call-signature); + --color-ts-index-signature: var(--light-color-ts-index-signature); + --color-ts-constructor-signature: var( + --light-color-ts-constructor-signature + ); + --color-ts-parameter: var(--light-color-ts-parameter); + --color-ts-type-parameter: var(--light-color-ts-type-parameter); + --color-ts-accessor: var(--light-color-ts-accessor); + --color-ts-get-signature: var(--light-color-ts-get-signature); + --color-ts-set-signature: var(--light-color-ts-set-signature); + --color-ts-type-alias: var(--light-color-ts-type-alias); + --color-document: var(--light-color-document); + + --external-icon: var(--light-external-icon); + --color-scheme: var(--light-color-scheme); +} + +:root[data-theme="dark"] { + --color-background: var(--dark-color-background); + --color-background-secondary: var(--dark-color-background-secondary); + --color-background-warning: var(--dark-color-background-warning); + --color-warning-text: var(--dark-color-warning-text); + --color-icon-background: var(--dark-color-icon-background); + --color-accent: var(--dark-color-accent); + --color-active-menu-item: var(--dark-color-active-menu-item); + --color-text: var(--dark-color-text); + --color-text-aside: var(--dark-color-text-aside); + --color-link: var(--dark-color-link); + --color-focus-outline: var(--dark-color-focus-outline); + + --color-ts-keyword: var(--dark-color-ts-keyword); + --color-ts-module: var(--dark-color-ts-module); + --color-ts-namespace: var(--dark-color-ts-namespace); + --color-ts-enum: var(--dark-color-ts-enum); + --color-ts-enum-member: var(--dark-color-ts-enum-member); + --color-ts-variable: var(--dark-color-ts-variable); + --color-ts-function: var(--dark-color-ts-function); + --color-ts-class: var(--dark-color-ts-class); + --color-ts-interface: var(--dark-color-ts-interface); + --color-ts-constructor: var(--dark-color-ts-constructor); + --color-ts-property: var(--dark-color-ts-property); + --color-ts-method: var(--dark-color-ts-method); + --color-ts-call-signature: var(--dark-color-ts-call-signature); + --color-ts-index-signature: var(--dark-color-ts-index-signature); + --color-ts-constructor-signature: var( + --dark-color-ts-constructor-signature + ); + --color-ts-parameter: var(--dark-color-ts-parameter); + --color-ts-type-parameter: var(--dark-color-ts-type-parameter); + --color-ts-accessor: var(--dark-color-ts-accessor); + --color-ts-get-signature: var(--dark-color-ts-get-signature); + --color-ts-set-signature: var(--dark-color-ts-set-signature); + --color-ts-type-alias: var(--dark-color-ts-type-alias); + --color-document: var(--dark-color-document); + + --external-icon: var(--dark-external-icon); + --color-scheme: var(--dark-color-scheme); +} + +*:focus-visible, +.tsd-accordion-summary:focus-visible svg { + outline: 2px solid var(--color-focus-outline); +} + +.always-visible, +.always-visible .tsd-signatures { + display: inherit !important; +} + +h1, +h2, +h3, +h4, +h5, +h6 { + line-height: 1.2; +} + +h1 { + font-size: 1.875rem; + margin: 0.67rem 0; +} + +h2 { + font-size: 1.5rem; + margin: 0.83rem 0; +} + +h3 { + font-size: 1.25rem; + margin: 1rem 0; +} + +h4 { + font-size: 1.05rem; + margin: 1.33rem 0; +} + +h5 { + font-size: 1rem; + margin: 1.5rem 0; +} + +h6 { + font-size: 0.875rem; + margin: 2.33rem 0; +} + +dl, +menu, +ol, +ul { + margin: 1em 0; +} + +dd { + margin: 0 0 0 40px; +} + +.container { + max-width: 1700px; + padding: 0 2rem; +} + +/* Footer */ +footer { + border-top: 1px solid var(--color-accent); + padding-top: 1rem; + padding-bottom: 1rem; + max-height: 3.5rem; +} +footer > p { + margin: 0 1em; +} + +.container-main { + margin: 0 auto; + /* toolbar, footer, margin */ + min-height: calc(100vh - 41px - 56px - 4rem); +} + +@keyframes fade-in { + from { + opacity: 0; + } + to { + opacity: 1; + } +} +@keyframes fade-out { + from { + opacity: 1; + visibility: visible; + } + to { + opacity: 0; + } +} +@keyframes fade-in-delayed { + 0% { + opacity: 0; + } + 33% { + opacity: 0; + } + 100% { + opacity: 1; + } +} +@keyframes fade-out-delayed { + 0% { + opacity: 1; + visibility: visible; + } + 66% { + opacity: 0; + } + 100% { + opacity: 0; + } +} +@keyframes pop-in-from-right { + from { + transform: translate(100%, 0); + } + to { + transform: translate(0, 0); + } +} +@keyframes pop-out-to-right { + from { + transform: translate(0, 0); + visibility: visible; + } + to { + transform: translate(100%, 0); + } +} +body { + background: var(--color-background); + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans", + Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji"; + font-size: 16px; + color: var(--color-text); +} + +a { + color: var(--color-link); + text-decoration: none; +} +a:hover { + text-decoration: underline; +} +a.external[target="_blank"] { + background-image: var(--external-icon); + background-position: top 3px right; + background-repeat: no-repeat; + padding-right: 13px; +} +a.tsd-anchor-link { + color: var(--color-text); +} + +code, +pre { + font-family: Menlo, Monaco, Consolas, "Courier New", monospace; + padding: 0.2em; + margin: 0; + font-size: 0.875rem; + border-radius: 0.8em; +} + +pre { + position: relative; + white-space: pre; + white-space: pre-wrap; + word-wrap: break-word; + padding: 10px; + border: 1px solid var(--color-accent); +} +pre code { + padding: 0; + font-size: 100%; +} +pre > button { + position: absolute; + top: 10px; + right: 10px; + opacity: 0; + transition: opacity 0.1s; + box-sizing: border-box; +} +pre:hover > button, +pre > button.visible { + opacity: 1; +} + +blockquote { + margin: 1em 0; + padding-left: 1em; + border-left: 4px solid gray; +} + +.tsd-typography { + line-height: 1.333em; +} +.tsd-typography ul { + list-style: square; + padding: 0 0 0 20px; + margin: 0; +} +.tsd-typography .tsd-index-panel h3, +.tsd-index-panel .tsd-typography h3, +.tsd-typography h4, +.tsd-typography h5, +.tsd-typography h6 { + font-size: 1em; +} +.tsd-typography h5, +.tsd-typography h6 { + font-weight: normal; +} +.tsd-typography p, +.tsd-typography ul, +.tsd-typography ol { + margin: 1em 0; +} +.tsd-typography table { + border-collapse: collapse; + border: none; +} +.tsd-typography td, +.tsd-typography th { + padding: 6px 13px; + border: 1px solid var(--color-accent); +} +.tsd-typography thead, +.tsd-typography tr:nth-child(even) { + background-color: var(--color-background-secondary); +} + +.tsd-breadcrumb { + margin: 0; + padding: 0; + color: var(--color-text-aside); +} +.tsd-breadcrumb a { + color: var(--color-text-aside); + text-decoration: none; +} +.tsd-breadcrumb a:hover { + text-decoration: underline; +} +.tsd-breadcrumb li { + display: inline; +} +.tsd-breadcrumb li:after { + content: " / "; +} + +.tsd-comment-tags { + display: flex; + flex-direction: column; +} +dl.tsd-comment-tag-group { + display: flex; + align-items: center; + overflow: hidden; + margin: 0.5em 0; +} +dl.tsd-comment-tag-group dt { + display: flex; + margin-right: 0.5em; + font-size: 0.875em; + font-weight: normal; +} +dl.tsd-comment-tag-group dd { + margin: 0; +} +code.tsd-tag { + padding: 0.25em 0.4em; + border: 0.1em solid var(--color-accent); + margin-right: 0.25em; + font-size: 70%; +} +h1 code.tsd-tag:first-of-type { + margin-left: 0.25em; +} + +dl.tsd-comment-tag-group dd:before, +dl.tsd-comment-tag-group dd:after { + content: " "; +} +dl.tsd-comment-tag-group dd pre, +dl.tsd-comment-tag-group dd:after { + clear: both; +} +dl.tsd-comment-tag-group p { + margin: 0; +} + +.tsd-panel.tsd-comment .lead { + font-size: 1.1em; + line-height: 1.333em; + margin-bottom: 2em; +} +.tsd-panel.tsd-comment .lead:last-child { + margin-bottom: 0; +} + +.tsd-filter-visibility h4 { + font-size: 1rem; + padding-top: 0.75rem; + padding-bottom: 0.5rem; + margin: 0; +} +.tsd-filter-item:not(:last-child) { + margin-bottom: 0.5rem; +} +.tsd-filter-input { + display: flex; + width: -moz-fit-content; + width: fit-content; + align-items: center; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + cursor: pointer; +} +.tsd-filter-input input[type="checkbox"] { + cursor: pointer; + position: absolute; + width: 1.5em; + height: 1.5em; + opacity: 0; +} +.tsd-filter-input input[type="checkbox"]:disabled { + pointer-events: none; +} +.tsd-filter-input svg { + cursor: pointer; + width: 1.5em; + height: 1.5em; + margin-right: 0.5em; + border-radius: 0.33em; + /* Leaving this at full opacity breaks event listeners on Firefox. + Don't remove unless you know what you're doing. */ + opacity: 0.99; +} +.tsd-filter-input input[type="checkbox"]:focus-visible + svg { + outline: 2px solid var(--color-focus-outline); +} +.tsd-checkbox-background { + fill: var(--color-accent); +} +input[type="checkbox"]:checked ~ svg .tsd-checkbox-checkmark { + stroke: var(--color-text); +} +.tsd-filter-input input:disabled ~ svg > .tsd-checkbox-background { + fill: var(--color-background); + stroke: var(--color-accent); + stroke-width: 0.25rem; +} +.tsd-filter-input input:disabled ~ svg > .tsd-checkbox-checkmark { + stroke: var(--color-accent); +} + +.settings-label { + font-weight: bold; + text-transform: uppercase; + display: inline-block; +} + +.tsd-filter-visibility .settings-label { + margin: 0.75rem 0 0.5rem 0; +} + +.tsd-theme-toggle .settings-label { + margin: 0.75rem 0.75rem 0 0; +} + +.tsd-hierarchy { + list-style: square; + margin: 0; +} +.tsd-hierarchy .target { + font-weight: bold; +} + +.tsd-full-hierarchy:not(:last-child) { + margin-bottom: 1em; + padding-bottom: 1em; + border-bottom: 1px solid var(--color-accent); +} +.tsd-full-hierarchy, +.tsd-full-hierarchy ul { + list-style: none; + margin: 0; + padding: 0; +} +.tsd-full-hierarchy ul { + padding-left: 1.5rem; +} +.tsd-full-hierarchy a { + padding: 0.25rem 0 !important; + font-size: 1rem; + display: inline-flex; + align-items: center; + color: var(--color-text); +} + +.tsd-panel-group.tsd-index-group { + margin-bottom: 0; +} +.tsd-index-panel .tsd-index-list { + list-style: none; + line-height: 1.333em; + margin: 0; + padding: 0.25rem 0 0 0; + overflow: hidden; + display: grid; + grid-template-columns: repeat(3, 1fr); + column-gap: 1rem; + grid-template-rows: auto; +} +@media (max-width: 1024px) { + .tsd-index-panel .tsd-index-list { + grid-template-columns: repeat(2, 1fr); + } +} +@media (max-width: 768px) { + .tsd-index-panel .tsd-index-list { + grid-template-columns: repeat(1, 1fr); + } +} +.tsd-index-panel .tsd-index-list li { + -webkit-page-break-inside: avoid; + -moz-page-break-inside: avoid; + -ms-page-break-inside: avoid; + -o-page-break-inside: avoid; + page-break-inside: avoid; +} + +.tsd-flag { + display: inline-block; + padding: 0.25em 0.4em; + border-radius: 4px; + color: var(--color-comment-tag-text); + background-color: var(--color-comment-tag); + text-indent: 0; + font-size: 75%; + line-height: 1; + font-weight: normal; +} + +.tsd-anchor { + position: relative; + top: -100px; +} + +.tsd-member { + position: relative; +} +.tsd-member .tsd-anchor + h3 { + display: flex; + align-items: center; + margin-top: 0; + margin-bottom: 0; + border-bottom: none; +} + +.tsd-navigation.settings { + margin: 1rem 0; +} +.tsd-navigation > a, +.tsd-navigation .tsd-accordion-summary { + width: calc(100% - 0.25rem); + display: flex; + align-items: center; +} +.tsd-navigation a, +.tsd-navigation summary > span, +.tsd-page-navigation a { + display: flex; + width: calc(100% - 0.25rem); + align-items: center; + padding: 0.25rem; + color: var(--color-text); + text-decoration: none; + box-sizing: border-box; +} +.tsd-navigation a.current, +.tsd-page-navigation a.current { + background: var(--color-active-menu-item); +} +.tsd-navigation a:hover, +.tsd-page-navigation a:hover { + text-decoration: underline; +} +.tsd-navigation ul, +.tsd-page-navigation ul { + margin-top: 0; + margin-bottom: 0; + padding: 0; + list-style: none; +} +.tsd-navigation li, +.tsd-page-navigation li { + padding: 0; + max-width: 100%; +} +.tsd-navigation .tsd-nav-link { + display: none; +} +.tsd-nested-navigation { + margin-left: 3rem; +} +.tsd-nested-navigation > li > details { + margin-left: -1.5rem; +} +.tsd-small-nested-navigation { + margin-left: 1.5rem; +} +.tsd-small-nested-navigation > li > details { + margin-left: -1.5rem; +} + +.tsd-page-navigation-section { + margin-left: 10px; +} +.tsd-page-navigation-section > summary { + padding: 0.25rem; +} +.tsd-page-navigation-section > div { + margin-left: 20px; +} +.tsd-page-navigation ul { + padding-left: 1.75rem; +} + +#tsd-sidebar-links a { + margin-top: 0; + margin-bottom: 0.5rem; + line-height: 1.25rem; +} +#tsd-sidebar-links a:last-of-type { + margin-bottom: 0; +} + +a.tsd-index-link { + padding: 0.25rem 0 !important; + font-size: 1rem; + line-height: 1.25rem; + display: inline-flex; + align-items: center; + color: var(--color-text); +} +.tsd-accordion-summary { + list-style-type: none; /* hide marker on non-safari */ + outline: none; /* broken on safari, so just hide it */ +} +.tsd-accordion-summary::-webkit-details-marker { + display: none; /* hide marker on safari */ +} +.tsd-accordion-summary, +.tsd-accordion-summary a { + -moz-user-select: none; + -webkit-user-select: none; + -ms-user-select: none; + user-select: none; + + cursor: pointer; +} +.tsd-accordion-summary a { + width: calc(100% - 1.5rem); +} +.tsd-accordion-summary > * { + margin-top: 0; + margin-bottom: 0; + padding-top: 0; + padding-bottom: 0; +} +.tsd-accordion .tsd-accordion-summary > svg { + margin-left: 0.25rem; + vertical-align: text-top; +} +.tsd-index-content > :not(:first-child) { + margin-top: 0.75rem; +} +.tsd-index-heading { + margin-top: 1.5rem; + margin-bottom: 0.75rem; +} + +.tsd-kind-icon { + margin-right: 0.5rem; + width: 1.25rem; + height: 1.25rem; + min-width: 1.25rem; + min-height: 1.25rem; +} +.tsd-kind-icon path { + transform-origin: center; + transform: scale(1.1); +} +.tsd-signature > .tsd-kind-icon { + margin-right: 0.8rem; +} + +.tsd-panel { + margin-bottom: 2.5rem; +} +.tsd-panel.tsd-member { + margin-bottom: 4rem; +} +.tsd-panel:empty { + display: none; +} +.tsd-panel > h1, +.tsd-panel > h2, +.tsd-panel > h3 { + margin: 1.5rem -1.5rem 0.75rem -1.5rem; + padding: 0 1.5rem 0.75rem 1.5rem; +} +.tsd-panel > h1.tsd-before-signature, +.tsd-panel > h2.tsd-before-signature, +.tsd-panel > h3.tsd-before-signature { + margin-bottom: 0; + border-bottom: none; +} + +.tsd-panel-group { + margin: 2rem 0; +} +.tsd-panel-group.tsd-index-group { + margin: 2rem 0; +} +.tsd-panel-group.tsd-index-group details { + margin: 2rem 0; +} +.tsd-panel-group > .tsd-accordion-summary { + margin-bottom: 1rem; +} + +#tsd-search { + transition: background-color 0.2s; +} +#tsd-search .title { + position: relative; + z-index: 2; +} +#tsd-search .field { + position: absolute; + left: 0; + top: 0; + right: 2.5rem; + height: 100%; +} +#tsd-search .field input { + box-sizing: border-box; + position: relative; + top: -50px; + z-index: 1; + width: 100%; + padding: 0 10px; + opacity: 0; + outline: 0; + border: 0; + background: transparent; + color: var(--color-text); +} +#tsd-search .field label { + position: absolute; + overflow: hidden; + right: -40px; +} +#tsd-search .field input, +#tsd-search .title, +#tsd-toolbar-links a { + transition: opacity 0.2s; +} +#tsd-search .results { + position: absolute; + visibility: hidden; + top: 40px; + width: 100%; + margin: 0; + padding: 0; + list-style: none; + box-shadow: 0 0 4px rgba(0, 0, 0, 0.25); +} +#tsd-search .results li { + background-color: var(--color-background); + line-height: initial; + padding: 4px; +} +#tsd-search .results li:nth-child(even) { + background-color: var(--color-background-secondary); +} +#tsd-search .results li.state { + display: none; +} +#tsd-search .results li.current:not(.no-results), +#tsd-search .results li:hover:not(.no-results) { + background-color: var(--color-accent); +} +#tsd-search .results a { + display: flex; + align-items: center; + padding: 0.25rem; + box-sizing: border-box; +} +#tsd-search .results a:before { + top: 10px; +} +#tsd-search .results span.parent { + color: var(--color-text-aside); + font-weight: normal; +} +#tsd-search.has-focus { + background-color: var(--color-accent); +} +#tsd-search.has-focus .field input { + top: 0; + opacity: 1; +} +#tsd-search.has-focus .title, +#tsd-search.has-focus #tsd-toolbar-links a { + z-index: 0; + opacity: 0; +} +#tsd-search.has-focus .results { + visibility: visible; +} +#tsd-search.loading .results li.state.loading { + display: block; +} +#tsd-search.failure .results li.state.failure { + display: block; +} + +#tsd-toolbar-links { + position: absolute; + top: 0; + right: 2rem; + height: 100%; + display: flex; + align-items: center; + justify-content: flex-end; +} +#tsd-toolbar-links a { + margin-left: 1.5rem; +} +#tsd-toolbar-links a:hover { + text-decoration: underline; +} + +.tsd-signature { + margin: 0 0 1rem 0; + padding: 1rem 0.5rem; + border: 1px solid var(--color-accent); + font-family: Menlo, Monaco, Consolas, "Courier New", monospace; + font-size: 14px; + overflow-x: auto; +} + +.tsd-signature-keyword { + color: var(--color-ts-keyword); + font-weight: normal; +} + +.tsd-signature-symbol { + color: var(--color-text-aside); + font-weight: normal; +} + +.tsd-signature-type { + font-style: italic; + font-weight: normal; +} + +.tsd-signatures { + padding: 0; + margin: 0 0 1em 0; + list-style-type: none; +} +.tsd-signatures .tsd-signature { + margin: 0; + border-color: var(--color-accent); + border-width: 1px 0; + transition: background-color 0.1s; +} +.tsd-signatures .tsd-index-signature:not(:last-child) { + margin-bottom: 1em; +} +.tsd-signatures .tsd-index-signature .tsd-signature { + border-width: 1px; +} +.tsd-description .tsd-signatures .tsd-signature { + border-width: 1px; +} + +ul.tsd-parameter-list, +ul.tsd-type-parameter-list { + list-style: square; + margin: 0; + padding-left: 20px; +} +ul.tsd-parameter-list > li.tsd-parameter-signature, +ul.tsd-type-parameter-list > li.tsd-parameter-signature { + list-style: none; + margin-left: -20px; +} +ul.tsd-parameter-list h5, +ul.tsd-type-parameter-list h5 { + font-size: 16px; + margin: 1em 0 0.5em 0; +} +.tsd-sources { + margin-top: 1rem; + font-size: 0.875em; +} +.tsd-sources a { + color: var(--color-text-aside); + text-decoration: underline; +} +.tsd-sources ul { + list-style: none; + padding: 0; +} + +.tsd-page-toolbar { + position: sticky; + z-index: 1; + top: 0; + left: 0; + width: 100%; + color: var(--color-text); + background: var(--color-background-secondary); + border-bottom: 1px var(--color-accent) solid; + transition: transform 0.3s ease-in-out; +} +.tsd-page-toolbar a { + color: var(--color-text); + text-decoration: none; +} +.tsd-page-toolbar a.title { + font-weight: bold; +} +.tsd-page-toolbar a.title:hover { + text-decoration: underline; +} +.tsd-page-toolbar .tsd-toolbar-contents { + display: flex; + justify-content: space-between; + height: 2.5rem; + margin: 0 auto; +} +.tsd-page-toolbar .table-cell { + position: relative; + white-space: nowrap; + line-height: 40px; +} +.tsd-page-toolbar .table-cell:first-child { + width: 100%; +} +.tsd-page-toolbar .tsd-toolbar-icon { + box-sizing: border-box; + line-height: 0; + padding: 12px 0; +} + +.tsd-widget { + display: inline-block; + overflow: hidden; + opacity: 0.8; + height: 40px; + transition: + opacity 0.1s, + background-color 0.2s; + vertical-align: bottom; + cursor: pointer; +} +.tsd-widget:hover { + opacity: 0.9; +} +.tsd-widget.active { + opacity: 1; + background-color: var(--color-accent); +} +.tsd-widget.no-caption { + width: 40px; +} +.tsd-widget.no-caption:before { + margin: 0; +} + +.tsd-widget.options, +.tsd-widget.menu { + display: none; +} +input[type="checkbox"] + .tsd-widget:before { + background-position: -120px 0; +} +input[type="checkbox"]:checked + .tsd-widget:before { + background-position: -160px 0; +} + +img { + max-width: 100%; +} + +.tsd-anchor-icon { + display: inline-flex; + align-items: center; + margin-left: 0.5rem; + vertical-align: middle; + color: var(--color-text); +} + +.tsd-anchor-icon svg { + width: 1em; + height: 1em; + visibility: hidden; +} + +.tsd-anchor-link:hover > .tsd-anchor-icon svg { + visibility: visible; +} + +.deprecated { + text-decoration: line-through !important; +} + +.warning { + padding: 1rem; + color: var(--color-warning-text); + background: var(--color-background-warning); +} + +.tsd-kind-project { + color: var(--color-ts-project); +} +.tsd-kind-module { + color: var(--color-ts-module); +} +.tsd-kind-namespace { + color: var(--color-ts-namespace); +} +.tsd-kind-enum { + color: var(--color-ts-enum); +} +.tsd-kind-enum-member { + color: var(--color-ts-enum-member); +} +.tsd-kind-variable { + color: var(--color-ts-variable); +} +.tsd-kind-function { + color: var(--color-ts-function); +} +.tsd-kind-class { + color: var(--color-ts-class); +} +.tsd-kind-interface { + color: var(--color-ts-interface); +} +.tsd-kind-constructor { + color: var(--color-ts-constructor); +} +.tsd-kind-property { + color: var(--color-ts-property); +} +.tsd-kind-method { + color: var(--color-ts-method); +} +.tsd-kind-call-signature { + color: var(--color-ts-call-signature); +} +.tsd-kind-index-signature { + color: var(--color-ts-index-signature); +} +.tsd-kind-constructor-signature { + color: var(--color-ts-constructor-signature); +} +.tsd-kind-parameter { + color: var(--color-ts-parameter); +} +.tsd-kind-type-literal { + color: var(--color-ts-type-literal); +} +.tsd-kind-type-parameter { + color: var(--color-ts-type-parameter); +} +.tsd-kind-accessor { + color: var(--color-ts-accessor); +} +.tsd-kind-get-signature { + color: var(--color-ts-get-signature); +} +.tsd-kind-set-signature { + color: var(--color-ts-set-signature); +} +.tsd-kind-type-alias { + color: var(--color-ts-type-alias); +} + +/* if we have a kind icon, don't color the text by kind */ +.tsd-kind-icon ~ span { + color: var(--color-text); +} + +* { + scrollbar-width: thin; + scrollbar-color: var(--color-accent) var(--color-icon-background); +} + +*::-webkit-scrollbar { + width: 0.75rem; +} + +*::-webkit-scrollbar-track { + background: var(--color-icon-background); +} + +*::-webkit-scrollbar-thumb { + background-color: var(--color-accent); + border-radius: 999rem; + border: 0.25rem solid var(--color-icon-background); +} + +/* mobile */ +@media (max-width: 769px) { + .tsd-widget.options, + .tsd-widget.menu { + display: inline-block; + } + + .container-main { + display: flex; + } + html .col-content { + float: none; + max-width: 100%; + width: 100%; + } + html .col-sidebar { + position: fixed !important; + overflow-y: auto; + -webkit-overflow-scrolling: touch; + z-index: 1024; + top: 0 !important; + bottom: 0 !important; + left: auto !important; + right: 0 !important; + padding: 1.5rem 1.5rem 0 0; + width: 75vw; + visibility: hidden; + background-color: var(--color-background); + transform: translate(100%, 0); + } + html .col-sidebar > *:last-child { + padding-bottom: 20px; + } + html .overlay { + content: ""; + display: block; + position: fixed; + z-index: 1023; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: rgba(0, 0, 0, 0.75); + visibility: hidden; + } + + .to-has-menu .overlay { + animation: fade-in 0.4s; + } + + .to-has-menu .col-sidebar { + animation: pop-in-from-right 0.4s; + } + + .from-has-menu .overlay { + animation: fade-out 0.4s; + } + + .from-has-menu .col-sidebar { + animation: pop-out-to-right 0.4s; + } + + .has-menu body { + overflow: hidden; + } + .has-menu .overlay { + visibility: visible; + } + .has-menu .col-sidebar { + visibility: visible; + transform: translate(0, 0); + display: flex; + flex-direction: column; + gap: 1.5rem; + max-height: 100vh; + padding: 1rem 2rem; + } + .has-menu .tsd-navigation { + max-height: 100%; + } + #tsd-toolbar-links { + display: none; + } + .tsd-navigation .tsd-nav-link { + display: flex; + } +} + +/* one sidebar */ +@media (min-width: 770px) { + .container-main { + display: grid; + grid-template-columns: minmax(0, 1fr) minmax(0, 2fr); + grid-template-areas: "sidebar content"; + margin: 2rem auto; + } + + .col-sidebar { + grid-area: sidebar; + } + .col-content { + grid-area: content; + padding: 0 1rem; + } +} +@media (min-width: 770px) and (max-width: 1399px) { + .col-sidebar { + max-height: calc(100vh - 2rem - 42px); + overflow: auto; + position: sticky; + top: 42px; + padding-top: 1rem; + } + .site-menu { + margin-top: 1rem; + } +} + +/* two sidebars */ +@media (min-width: 1200px) { + .container-main { + grid-template-columns: minmax(0, 1fr) minmax(0, 2.5fr) minmax(0, 20rem); + grid-template-areas: "sidebar content toc"; + } + + .col-sidebar { + display: contents; + } + + .page-menu { + grid-area: toc; + padding-left: 1rem; + } + .site-menu { + grid-area: sidebar; + } + + .site-menu { + margin-top: 1rem 0; + } + + .page-menu, + .site-menu { + max-height: calc(100vh - 2rem - 42px); + overflow: auto; + position: sticky; + top: 42px; + } +} diff --git a/Documentation/index.html b/Documentation/index.html new file mode 100644 index 00000000..560eba1e --- /dev/null +++ b/Documentation/index.html @@ -0,0 +1,313 @@ +@oxc-resolver/binding

@oxc-resolver/binding

+ + + OXC Logo + +

+
+

Crates.io +npmjs.com

+

Docs.rs +Build Status +Code Coverage +CodSpeed Badge +Sponsors +Discord chat +MIT licensed

+
+

Oxc Resolver

Rust port of enhanced-resolve.

+ +

The following usages apply to both Rust and Node.js; the code snippets are written in JavaScript.

+

To handle the exports field in package.json, ESM and CJS need to be differentiated.

+

Per ESM Resolution algorithm

+
+

defaultConditions is the conditional environment name array, ["node", "import"].

+
+

This means when the caller is an ESM import (import "module"), resolve options should be

+
{
"conditionNames": ["node", "import"]
} +
+ +

Per CJS Resolution algorithm

+
+

LOAD_PACKAGE_EXPORTS(X, DIR)

+
    +
  1. let MATCH = PACKAGE_EXPORTS_RESOLVE(pathToFileURL(DIR/NAME), "." + SUBPATH, +package.json "exports", ["node", "require"]) defined in the ESM resolver.
  2. +
+
+

This means when the caller is a CJS require (require("module")), resolve options should be

+
{
"conditionNames": ["node", "require"]
} +
+ +

To support both CJS and ESM with the same cache:

+
const esmResolver = ResolverFactory({
conditionNames: ["node", "import"]
});

const cjsResolver = esmResolver.cloneWithOptions({
conditionNames: ["node", "require"]
}); +
+ +

From this non-standard spec:

+
+

The browser field is provided to JavaScript bundlers or component tools when packaging modules for client side use.

+
+

The option is

+
{
"aliasFields": ["browser"]
} +
+ +
{
"mainFields": ["module", "main"]
} +
+ +

Quoting esbuild's documentation:

+
    +
  • main - This is the standard field for all packages that are meant to be used with node. The name main is hard-coded in to node's module resolution logic itself. Because it's intended for use with node, it's reasonable to expect that the file path in this field is a CommonJS-style module.
  • +
  • module - This field came from a proposal for how to integrate ECMAScript modules into node. Because of this, it's reasonable to expect that the file path in this field is an ECMAScript-style module. This proposal wasn't adopted by node (node uses "type": "module" instead) but it was adopted by major bundlers because ECMAScript-style modules lead to better tree shaking, or dead code removal.
  • +
  • browser - This field came from a proposal that allows bundlers to replace node-specific files or modules with their browser-friendly versions. It lets you specify an alternate browser-specific entry point. Note that it is possible for a package to use both the browser and module field together (see the note below).
  • +
+
    +
  • Error: Package subpath '.' is not defined by "exports" in - occurs when resolving without conditionNames.
  • +
+

The options are aligned with enhanced-resolve.

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldDefaultDescription
alias[]A list of module alias configurations or an object which maps key to value
aliasFields[]A list of alias fields in description files
extensionAlias{}An object which maps extension to extension aliases
conditionNames[]A list of exports field condition names
descriptionFiles["package.json"]A list of description files to read from
enforceExtensionfalseEnforce that a extension from extensions must be used
exportsFields["exports"]A list of exports fields in description files
extensions[".js", ".json", ".node"]A list of extensions which should be tried for files
fallback[]Same as alias, but only used if default resolving fails
fileSystemThe file system which should be used
fullySpecifiedfalseRequest passed to resolve is already fully specified and extensions or main files are not resolved for it (they are still resolved for internal requests)
mainFields["main"]A list of main fields in description files
mainFiles["index"]A list of main files in directories
modules["node_modules"]A list of directories to resolve modules from, can be absolute path or folder name
resolveToContextfalseResolve to a context instead of a file
preferRelativefalsePrefer to resolve module requests as relative request and fallback to resolving as module
preferAbsolutefalsePrefer to resolve server-relative urls as absolute paths before falling back to resolve in roots
restrictions[]A list of resolve restrictions
roots[]A list of root paths
symlinkstrueWhether to resolve symlinks to their symlinked location
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldDefaultDescription
cachePredicatefunction() { return true };A function which decides whether a request should be cached or not. An object is passed to the function with path and request properties.
cacheWithContexttrueIf unsafe cache is enabled, includes request.context in the cache key
plugins[]A list of additional resolve plugins which should be applied
resolverundefinedA prepared Resolver to which the plugins are attached
unsafeCachefalseUse this cache object to unsafely cache the successful requests
+

The following environment variable emits tracing information for the oxc_resolver::resolve function.

+

e.g.

+
2024-06-11T07:12:20.003537Z DEBUG oxc_resolver: options: ResolveOptions { ... }, path: "...", specifier: "...", ret: "..."
at /path/to/oxc_resolver-1.8.1/src/lib.rs:212
in oxc_resolver::resolve with path: "...", specifier: "..." +
+ +

The input values are options, path and specifier, the returned value is ret.

+
OXC_LOG=DEBUG your_program
+
+ +
RD_LOG='oxc_resolver' rolldown build
+
+ +
RSPACK_PROFILE='TRACE=filter=oxc_resolver=trace&layer=logger' rspack build
+
+ +

Tests are ported from

+ +

Test cases are located in ./src/tests, fixtures are located in ./tests

+
    +
  • [x] alias.test.js
  • +
  • [x] browserField.test.js
  • +
  • [x] dependencies.test.js
  • +
  • [x] exportsField.test.js
  • +
  • [x] extension-alias.test.js
  • +
  • [x] extensions.test.js
  • +
  • [x] fallback.test.js
  • +
  • [x] fullSpecified.test.js
  • +
  • [x] identifier.test.js (see unit test in crates/oxc_resolver/src/request.rs)
  • +
  • [x] importsField.test.js
  • +
  • [x] incorrect-description-file.test.js (need to add ctx.fileDependencies)
  • +
  • [x] missing.test.js
  • +
  • [x] path.test.js (see unit test in crates/oxc_resolver/src/path.rs)
  • +
  • [ ] plugins.test.js
  • +
  • [ ] pnp.test.js
  • +
  • [x] resolve.test.js
  • +
  • [x] restrictions.test.js (partially done, regex is not supported yet)
  • +
  • [x] roots.test.js
  • +
  • [x] scoped-packages.test.js
  • +
  • [x] simple.test.js
  • +
  • [x] symlink.test.js
  • +
+

Irrelevant tests

+
    +
  • CachedInputFileSystem.test.js
  • +
  • SyncAsyncFileSystemDecorator.test.js
  • +
  • forEachBail.test.js
  • +
  • getPaths.test.js
  • +
  • pr-53.test.js
  • +
  • unsafe-cache.test.js
  • +
  • yield.test.js
  • +
+

+ + My sponsors + +

+

oxc_resolver is free and open-source software licensed under the MIT License.

+

Oxc partially copies code from the following projects.

+ + + + + + + + + + + + + + + + + + + + + + + + + +
ProjectLicense
webpack/enhanced-resolveMIT
dividab/tsconfig-pathsMIT
parcel-bundler/parcelMIT
tmccombs/json-comments-rsApache 2.0
+
diff --git a/Documentation/media/LICENSE b/Documentation/media/LICENSE new file mode 100644 index 00000000..e8308cad --- /dev/null +++ b/Documentation/media/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023-present Boshen + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Documentation/modules.html b/Documentation/modules.html new file mode 100644 index 00000000..b8f3a07a --- /dev/null +++ b/Documentation/modules.html @@ -0,0 +1 @@ +@oxc-resolver/binding

@oxc-resolver/binding

diff --git a/Source/builtins.rs b/Source/builtins.rs new file mode 100644 index 00000000..45a25117 --- /dev/null +++ b/Source/builtins.rs @@ -0,0 +1,71 @@ +/// Node.js built-in modules +/// +/// `node -p "[...require('module').builtinModules].map(b => JSON.stringify(b)).join(',\n')"` +/// +pub const NODEJS_BUILTINS: &[&str] = &[ + "_http_agent", + "_http_client", + "_http_common", + "_http_incoming", + "_http_outgoing", + "_http_server", + "_stream_duplex", + "_stream_passthrough", + "_stream_readable", + "_stream_transform", + "_stream_wrap", + "_stream_writable", + "_tls_common", + "_tls_wrap", + "assert", + "assert/strict", + "async_hooks", + "buffer", + "child_process", + "cluster", + "console", + "constants", + "crypto", + "dgram", + "diagnostics_channel", + "dns", + "dns/promises", + "domain", + "events", + "fs", + "fs/promises", + "http", + "http2", + "https", + "inspector", + "module", + "net", + "os", + "path", + "path/posix", + "path/win32", + "perf_hooks", + "process", + "punycode", + "querystring", + "readline", + "repl", + "stream", + "stream/consumers", + "stream/promises", + "stream/web", + "string_decoder", + "sys", + "timers", + "timers/promises", + "tls", + "trace_events", + "tty", + "url", + "util", + "util/types", + "v8", + "vm", + "worker_threads", + "zlib", +]; diff --git a/Source/cache.rs b/Source/cache.rs new file mode 100644 index 00000000..6e600664 --- /dev/null +++ b/Source/cache.rs @@ -0,0 +1,354 @@ +use once_cell::sync::OnceCell as OnceLock; +use std::{ + borrow::{Borrow, Cow}, + convert::AsRef, + hash::{BuildHasherDefault, Hash, Hasher}, + io, + ops::Deref, + path::{Path, PathBuf}, + sync::Arc, +}; + +use dashmap::{DashMap, DashSet}; +use rustc_hash::FxHasher; + +use crate::{ + context::ResolveContext as Ctx, package_json::PackageJson, path::PathUtil, FileMetadata, + FileSystem, ResolveError, ResolveOptions, TsConfig, +}; + +#[derive(Default)] +pub struct Cache { + pub(crate) fs: Fs, + paths: DashSet>, + tsconfigs: DashMap, BuildHasherDefault>, +} + +impl Cache { + pub fn new(fs: Fs) -> Self { + Self { fs, paths: DashSet::default(), tsconfigs: DashMap::default() } + } + + pub fn clear(&self) { + self.paths.clear(); + self.tsconfigs.clear(); + } + + pub fn value(&self, path: &Path) -> CachedPath { + let hash = { + let mut hasher = FxHasher::default(); + path.hash(&mut hasher); + hasher.finish() + }; + if let Some(cache_entry) = self.paths.get((hash, path).borrow() as &dyn CacheKey) { + return cache_entry.clone(); + } + let parent = path.parent().map(|p| self.value(p)); + let data = CachedPath(Arc::new(CachedPathImpl::new( + hash, + path.to_path_buf().into_boxed_path(), + parent, + ))); + self.paths.insert(data.clone()); + data + } + + pub fn tsconfig Result<(), ResolveError>>( + &self, + root: bool, + path: &Path, + callback: F, // callback for modifying tsconfig with `extends` + ) -> Result, ResolveError> { + if let Some(tsconfig_ref) = self.tsconfigs.get(path) { + return Ok(Arc::clone(tsconfig_ref.value())); + } + let meta = self.fs.metadata(path).ok(); + let tsconfig_path = if meta.is_some_and(|m| m.is_file) { + Cow::Borrowed(path) + } else if meta.is_some_and(|m| m.is_dir) { + Cow::Owned(path.join("tsconfig.json")) + } else { + let mut os_string = path.to_path_buf().into_os_string(); + os_string.push(".json"); + Cow::Owned(PathBuf::from(os_string)) + }; + let mut tsconfig_string = self + .fs + .read_to_string(&tsconfig_path) + .map_err(|_| ResolveError::TsconfigNotFound(path.to_path_buf()))?; + let mut tsconfig = + TsConfig::parse(root, &tsconfig_path, &mut tsconfig_string).map_err(|error| { + ResolveError::from_serde_json_error(tsconfig_path.to_path_buf(), &error) + })?; + callback(&mut tsconfig)?; + let tsconfig = Arc::new(tsconfig.build()); + self.tsconfigs.insert(path.to_path_buf(), Arc::clone(&tsconfig)); + Ok(tsconfig) + } +} + +#[derive(Clone)] +pub struct CachedPath(Arc); + +impl Hash for CachedPath { + fn hash(&self, state: &mut H) { + self.0.hash.hash(state); + } +} + +impl PartialEq for CachedPath { + fn eq(&self, other: &Self) -> bool { + self.0.path == other.0.path + } +} +impl Eq for CachedPath {} + +impl Deref for CachedPath { + type Target = CachedPathImpl; + + fn deref(&self) -> &Self::Target { + self.0.as_ref() + } +} + +impl<'a> Borrow for CachedPath { + fn borrow(&self) -> &(dyn CacheKey + 'a) { + self + } +} + +impl AsRef for CachedPath { + fn as_ref(&self) -> &CachedPathImpl { + self.0.as_ref() + } +} + +impl CacheKey for CachedPath { + fn tuple(&self) -> (u64, &Path) { + (self.hash, &self.path) + } +} + +pub struct CachedPathImpl { + hash: u64, + path: Box, + parent: Option, + meta: OnceLock>, + canonicalized: OnceLock>, + node_modules: OnceLock>, + package_json: OnceLock>>, +} + +impl CachedPathImpl { + fn new(hash: u64, path: Box, parent: Option) -> Self { + Self { + hash, + path, + parent, + meta: OnceLock::new(), + canonicalized: OnceLock::new(), + node_modules: OnceLock::new(), + package_json: OnceLock::new(), + } + } + + pub fn path(&self) -> &Path { + &self.path + } + + pub fn to_path_buf(&self) -> PathBuf { + self.path.to_path_buf() + } + + pub fn parent(&self) -> Option<&CachedPath> { + self.parent.as_ref() + } + + fn meta(&self, fs: &Fs) -> Option { + *self.meta.get_or_init(|| fs.metadata(&self.path).ok()) + } + + pub fn is_file(&self, fs: &Fs, ctx: &mut Ctx) -> bool { + if let Some(meta) = self.meta(fs) { + ctx.add_file_dependency(self.path()); + meta.is_file + } else { + ctx.add_missing_dependency(self.path()); + false + } + } + + pub fn is_dir(&self, fs: &Fs, ctx: &mut Ctx) -> bool { + self.meta(fs).map_or_else( + || { + ctx.add_missing_dependency(self.path()); + false + }, + |meta| meta.is_dir, + ) + } + + pub fn realpath(&self, fs: &Fs) -> io::Result { + self.canonicalized + .get_or_try_init(|| { + if fs.symlink_metadata(&self.path).is_ok_and(|m| m.is_symlink) { + return fs.canonicalize(&self.path).map(Some); + } + if let Some(parent) = self.parent() { + let parent_path = parent.realpath(fs)?; + return Ok(Some( + parent_path.normalize_with(self.path.strip_prefix(&parent.path).unwrap()), + )); + }; + Ok(None) + }) + .cloned() + .map(|r| r.unwrap_or_else(|| self.path.clone().to_path_buf())) + } + + pub fn module_directory( + &self, + module_name: &str, + cache: &Cache, + ctx: &mut Ctx, + ) -> Option { + let cached_path = cache.value(&self.path.join(module_name)); + cached_path.is_dir(&cache.fs, ctx).then_some(cached_path) + } + + pub fn cached_node_modules( + &self, + cache: &Cache, + ctx: &mut Ctx, + ) -> Option { + self.node_modules.get_or_init(|| self.module_directory("node_modules", cache, ctx)).clone() + } + + /// Find package.json of a path by traversing parent directories. + /// + /// # Errors + /// + /// * [ResolveError::JSON] + pub fn find_package_json( + &self, + fs: &Fs, + options: &ResolveOptions, + ctx: &mut Ctx, + ) -> Result>, ResolveError> { + let mut cache_value = self; + // Go up directories when the querying path is not a directory + while !cache_value.is_dir(fs, ctx) { + if let Some(cv) = &cache_value.parent { + cache_value = cv.as_ref(); + } else { + break; + } + } + let mut cache_value = Some(cache_value); + while let Some(cv) = cache_value { + if let Some(package_json) = cv.package_json(fs, options, ctx)? { + return Ok(Some(Arc::clone(&package_json))); + } + cache_value = cv.parent.as_deref(); + } + Ok(None) + } + + /// Get package.json of the given path. + /// + /// # Errors + /// + /// * [ResolveError::JSON] + pub fn package_json( + &self, + fs: &Fs, + options: &ResolveOptions, + ctx: &mut Ctx, + ) -> Result>, ResolveError> { + // Change to `std::sync::OnceLock::get_or_try_init` when it is stable. + let result = self + .package_json + .get_or_try_init(|| { + let package_json_path = self.path.join("package.json"); + let Ok(package_json_string) = fs.read_to_string(&package_json_path) else { + return Ok(None); + }; + let real_path = if options.symlinks { + self.realpath(fs)?.join("package.json") + } else { + package_json_path.clone() + }; + PackageJson::parse(package_json_path.clone(), real_path, &package_json_string) + .map(Arc::new) + .map(Some) + .map_err(|error| ResolveError::from_serde_json_error(package_json_path, &error)) + }) + .cloned(); + // https://github.com/webpack/enhanced-resolve/blob/58464fc7cb56673c9aa849e68e6300239601e615/lib/DescriptionFileUtils.js#L68-L82 + match &result { + Ok(Some(package_json)) => { + ctx.add_file_dependency(&package_json.path); + } + Ok(None) => { + // Avoid an allocation by making this lazy + if let Some(deps) = &mut ctx.missing_dependencies { + deps.push(self.path.join("package.json")); + } + } + Err(_) => { + if let Some(deps) = &mut ctx.file_dependencies { + deps.push(self.path.join("package.json")); + } + } + } + result + } +} + +/// Memoized cache key, code adapted from . +trait CacheKey { + fn tuple(&self) -> (u64, &Path); +} + +impl Hash for dyn CacheKey + '_ { + fn hash(&self, state: &mut H) { + self.tuple().0.hash(state); + } +} + +impl PartialEq for dyn CacheKey + '_ { + fn eq(&self, other: &Self) -> bool { + self.tuple().1 == other.tuple().1 + } +} + +impl Eq for dyn CacheKey + '_ {} + +impl<'a> CacheKey for (u64, &'a Path) { + fn tuple(&self) -> (u64, &Path) { + (self.0, self.1) + } +} + +impl<'a> Borrow for (u64, &'a Path) { + fn borrow(&self) -> &(dyn CacheKey + 'a) { + self + } +} + +/// Since the cache key is memoized, use an identity hasher +/// to avoid double cache. +#[derive(Default)] +struct IdentityHasher(u64); + +impl Hasher for IdentityHasher { + fn write(&mut self, _: &[u8]) { + unreachable!("Invalid use of IdentityHasher") + } + fn write_u64(&mut self, n: u64) { + self.0 = n; + } + fn finish(&self) -> u64 { + self.0 + } +} diff --git a/Source/context.rs b/Source/context.rs new file mode 100644 index 00000000..9349de30 --- /dev/null +++ b/Source/context.rs @@ -0,0 +1,89 @@ +use std::{ + ops::{Deref, DerefMut}, + path::{Path, PathBuf}, +}; + +use crate::error::ResolveError; + +#[derive(Debug, Default, Clone)] +pub struct ResolveContext(ResolveContextImpl); + +#[derive(Debug, Default, Clone)] +pub struct ResolveContextImpl { + pub fully_specified: bool, + + pub query: Option, + + pub fragment: Option, + + /// Files that was found on file system + pub file_dependencies: Option>, + + /// Files that was found on file system + pub missing_dependencies: Option>, + + /// The current resolving alias for bailing recursion alias. + pub resolving_alias: Option, + + /// For avoiding infinite recursion, which will cause stack overflow. + depth: u8, +} + +impl Deref for ResolveContext { + type Target = ResolveContextImpl; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for ResolveContext { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +impl ResolveContext { + pub fn with_fully_specified(&mut self, yes: bool) { + self.fully_specified = yes; + } + + pub fn with_query_fragment(&mut self, query: Option<&str>, fragment: Option<&str>) { + if let Some(query) = query { + self.query.replace(query.to_string()); + } + if let Some(fragment) = fragment { + self.fragment.replace(fragment.to_string()); + } + } + + pub fn init_file_dependencies(&mut self) { + self.file_dependencies.replace(vec![]); + self.missing_dependencies.replace(vec![]); + } + + pub fn add_file_dependency(&mut self, dep: &Path) { + if let Some(deps) = &mut self.file_dependencies { + deps.push(dep.to_path_buf()); + } + } + + pub fn add_missing_dependency(&mut self, dep: &Path) { + if let Some(deps) = &mut self.missing_dependencies { + deps.push(dep.to_path_buf()); + } + } + + pub fn with_resolving_alias(&mut self, alias: String) { + self.resolving_alias = Some(alias); + } + + pub fn test_for_infinite_recursion(&mut self) -> Result<(), ResolveError> { + self.depth += 1; + // 64 should be more than enough for detecting infinite recursion. + if self.depth > 64 { + return Err(ResolveError::Recursion); + } + Ok(()) + } +} diff --git a/Source/error.rs b/Source/error.rs new file mode 100644 index 00000000..b00a0ba8 --- /dev/null +++ b/Source/error.rs @@ -0,0 +1,182 @@ +use std::{io, path::PathBuf, sync::Arc}; +use thiserror::Error; + +/// All resolution errors +/// +/// `thiserror` is used to display meaningful error messages. +#[derive(Debug, Clone, PartialEq, Error)] +pub enum ResolveError { + /// Ignored path + /// + /// Derived from ignored path (false value) from browser field in package.json + /// ```json + /// { + /// "browser": { + /// "./module": false + /// } + /// } + /// ``` + /// See + #[error("Path is ignored {0}")] + Ignored(PathBuf), + + /// Module not found + #[error("Cannot find module '{0}'")] + NotFound(/* specifier */ String), + + /// Matched alias value not found + #[error("Cannot find module '{0}' for matched aliased key '{1}'")] + MatchedAliasNotFound(/* specifier */ String, /* alias key */ String), + + /// Tsconfig not found + #[error("Tsconfig not found {0}")] + TsconfigNotFound(PathBuf), + + /// Tsconfig's project reference path points to it self + #[error("Tsconfig's project reference path points to this tsconfig {0}")] + TsconfigSelfReference(PathBuf), + + #[error("{0}")] + IOError(IOError), + + /// Node.js builtin modules + /// + /// This is an error due to not being a Node.js runtime. + /// The `alias` option can be used to resolve a builtin module to a polyfill. + #[error("Builtin module {0}")] + Builtin(String), + + /// All of the aliased extension are not found + #[error("All of the aliased extensions are not found for {0}")] + ExtensionAlias(PathBuf), + + /// The provided path specifier cannot be parsed + #[error("{0}")] + Specifier(SpecifierError), + + /// JSON parse error + #[error("{0:?}")] + JSON(JSONError), + + /// Restricted by `ResolveOptions::restrictions` + #[error(r#"Path "{0}" restricted by {0}"#)] + Restriction(PathBuf, PathBuf), + + #[error(r#"Invalid module "{0}" specifier is not a valid subpath for the "exports" resolution of {1}"#)] + InvalidModuleSpecifier(String, PathBuf), + + #[error(r#"Invalid "exports" target "{0}" defined for '{1}' in the package config {2}"#)] + InvalidPackageTarget(String, String, PathBuf), + + #[error(r#"Package subpath '{0}' is not defined by "exports" in {1}"#)] + PackagePathNotExported(String, PathBuf), + + #[error(r#"Invalid package config "{0}", "exports" cannot contain some keys starting with '.' and some not. The exports object must either be an object of package subpath keys or an object of main entry condition name keys only."#)] + InvalidPackageConfig(PathBuf), + + #[error(r#"Default condition should be last one in "{0}""#)] + InvalidPackageConfigDefault(PathBuf), + + #[error(r#"Expecting folder to folder mapping. "{0}" should end with "/"#)] + InvalidPackageConfigDirectory(PathBuf), + + #[error(r#"Package import specifier "{0}" is not defined in package {1}"#)] + PackageImportNotDefined(String, PathBuf), + + #[error("{0} is unimplemented")] + Unimplemented(&'static str), + + /// Occurs when alias paths reference each other. + #[error("Recursion in resolving")] + Recursion, +} + +impl ResolveError { + pub fn is_ignore(&self) -> bool { + matches!(self, Self::Ignored(_)) + } + + pub(crate) fn from_serde_json_error(path: PathBuf, error: &serde_json::Error) -> Self { + Self::JSON(JSONError { + path, + message: error.to_string(), + line: error.line(), + column: error.column(), + }) + } +} + +/// Error for [ResolveError::Specifier] +#[derive(Debug, Clone, Eq, PartialEq, Error)] +pub enum SpecifierError { + #[error("The specifiers must be a non-empty string. Received \"{0}\"")] + Empty(String), +} + +/// JSON error from [serde_json::Error] +#[derive(Debug, Clone, Eq, PartialEq)] +pub struct JSONError { + pub path: PathBuf, + pub message: String, + pub line: usize, + pub column: usize, +} + +#[derive(Debug, Clone, Error)] +#[error("{0}")] +pub struct IOError(Arc); + +impl PartialEq for IOError { + fn eq(&self, other: &Self) -> bool { + self.0.kind() == other.0.kind() + } +} + +impl From for io::Error { + fn from(error: IOError) -> Self { + let io_error = error.0.as_ref(); + Self::new(io_error.kind(), io_error.to_string()) + } +} + +impl From for ResolveError { + fn from(err: io::Error) -> Self { + Self::IOError(IOError(Arc::new(err))) + } +} + +#[test] +fn test_into_io_error() { + use std::io::{self, ErrorKind}; + let error_string = "IOError occurred"; + let string_error = io::Error::new(ErrorKind::Interrupted, error_string.to_string()); + let string_error2 = io::Error::new(ErrorKind::Interrupted, error_string.to_string()); + let resolve_io_error: ResolveError = ResolveError::from(string_error2); + + assert_eq!(resolve_io_error, ResolveError::from(string_error)); + assert_eq!(resolve_io_error.clone(), resolve_io_error); + let ResolveError::IOError(io_error) = resolve_io_error else { unreachable!() }; + assert_eq!( + format!("{io_error:?}"), + r#"IOError(Custom { kind: Interrupted, error: "IOError occurred" })"# + ); + // fix for https://github.com/web-infra-dev/rspack/issues/4564 + let std_io_error: io::Error = io_error.into(); + assert_eq!(std_io_error.kind(), ErrorKind::Interrupted); + assert_eq!(std_io_error.to_string(), error_string); + assert_eq!( + format!("{std_io_error:?}"), + r#"Custom { kind: Interrupted, error: "IOError occurred" }"# + ); +} + +#[test] +fn test_coverage() { + let error = ResolveError::NotFound("x".into()); + assert_eq!(format!("{error:?}"), r#"NotFound("x")"#); + assert_eq!(error.clone(), error); + + let error = ResolveError::Specifier(SpecifierError::Empty("x".into())); + assert_eq!(format!("{error:?}"), r#"Specifier(Empty("x"))"#); + assert_eq!(error.clone(), error); +} diff --git a/Source/file_system.rs b/Source/file_system.rs new file mode 100644 index 00000000..d5f0ff8a --- /dev/null +++ b/Source/file_system.rs @@ -0,0 +1,220 @@ +use cfg_if::cfg_if; +use std::{ + fs, io, + path::{Path, PathBuf}, +}; + +#[cfg(feature = "yarn_pnp")] +use pnp::fs::{LruZipCache, VPath, VPathInfo, ZipCache}; + +/// File System abstraction used for `ResolverGeneric` +pub trait FileSystem: Send + Sync { + /// See [std::fs::read_to_string] + /// + /// # Errors + /// + /// * See [std::fs::read_to_string] + /// ## Warning + /// Use `&Path` instead of a generic `P: AsRef` here, + /// because object safety requirements, it is especially useful, when + /// you want to store multiple `dyn FileSystem` in a `Vec` or use a `ResolverGeneric` in + /// napi env. + fn read_to_string(&self, path: &Path) -> io::Result; + + /// See [std::fs::metadata] + /// + /// # Errors + /// See [std::fs::metadata] + /// ## Warning + /// Use `&Path` instead of a generic `P: AsRef` here, + /// because object safety requirements, it is especially useful, when + /// you want to store multiple `dyn FileSystem` in a `Vec` or use a `ResolverGeneric` in + /// napi env. + fn metadata(&self, path: &Path) -> io::Result; + + /// See [std::fs::symlink_metadata] + /// + /// # Errors + /// + /// See [std::fs::symlink_metadata] + /// ## Warning + /// Use `&Path` instead of a generic `P: AsRef` here, + /// because object safety requirements, it is especially useful, when + /// you want to store multiple `dyn FileSystem` in a `Vec` or use a `ResolverGeneric` in + /// napi env. + fn symlink_metadata(&self, path: &Path) -> io::Result; + + /// See [std::fs::canonicalize] + /// + /// # Errors + /// + /// See [std::fs::read_link] + /// ## Warning + /// Use `&Path` instead of a generic `P: AsRef` here, + /// because object safety requirements, it is especially useful, when + /// you want to store multiple `dyn FileSystem` in a `Vec` or use a `ResolverGeneric` in + /// napi env. + fn canonicalize(&self, path: &Path) -> io::Result; +} + +/// Metadata information about a file +#[derive(Debug, Clone, Copy)] +pub struct FileMetadata { + pub(crate) is_file: bool, + pub(crate) is_dir: bool, + pub(crate) is_symlink: bool, +} + +impl FileMetadata { + pub fn new(is_file: bool, is_dir: bool, is_symlink: bool) -> Self { + Self { is_file, is_dir, is_symlink } + } +} + +#[cfg(feature = "yarn_pnp")] +impl From for FileMetadata { + fn from(value: pnp::fs::FileType) -> Self { + Self::new(value == pnp::fs::FileType::File, value == pnp::fs::FileType::Directory, false) + } +} + +impl From for FileMetadata { + fn from(metadata: fs::Metadata) -> Self { + Self::new(metadata.is_file(), metadata.is_dir(), metadata.is_symlink()) + } +} + +/// Operating System +#[cfg(feature = "yarn_pnp")] +pub struct FileSystemOs { + pnp_lru: LruZipCache>, +} + +#[cfg(not(feature = "yarn_pnp"))] +pub struct FileSystemOs; + +impl Default for FileSystemOs { + fn default() -> Self { + cfg_if! { + if #[cfg(feature = "yarn_pnp")] { + Self { pnp_lru: LruZipCache::new(50, pnp::fs::open_zip_via_read_p) } + } else { + Self + } + } + } +} + +fn read_to_string(path: &Path) -> io::Result { + // `simdutf8` is faster than `std::str::from_utf8` which `fs::read_to_string` uses internally + let bytes = std::fs::read(path)?; + if simdutf8::basic::from_utf8(&bytes).is_err() { + // Same error as `fs::read_to_string` produces (`io::Error::INVALID_UTF8`) + return Err(io::Error::new( + io::ErrorKind::InvalidData, + "stream did not contain valid UTF-8", + )); + } + // SAFETY: `simdutf8` has ensured it's a valid UTF-8 string + Ok(unsafe { String::from_utf8_unchecked(bytes) }) +} + +impl FileSystem for FileSystemOs { + fn read_to_string(&self, path: &Path) -> io::Result { + cfg_if! { + if #[cfg(feature = "yarn_pnp")] { + match VPath::from(path)? { + VPath::Zip(info) => { + self.pnp_lru.read_to_string(info.physical_base_path(), info.zip_path) + } + VPath::Virtual(info) => read_to_string(&info.physical_base_path()), + VPath::Native(path) => read_to_string(&path), + } + } else { + read_to_string(path) + } + } + } + + fn metadata(&self, path: &Path) -> io::Result { + cfg_if! { + if #[cfg(feature = "yarn_pnp")] { + match VPath::from(path)? { + VPath::Zip(info) => self + .pnp_lru + .file_type(info.physical_base_path(), info.zip_path) + .map(FileMetadata::from), + VPath::Virtual(info) => { + fs::metadata(info.physical_base_path()).map(FileMetadata::from) + } + VPath::Native(path) => fs::metadata(path).map(FileMetadata::from), + } + } else { + fs::metadata(path).map(FileMetadata::from) + } + } + } + + fn symlink_metadata(&self, path: &Path) -> io::Result { + fs::symlink_metadata(path).map(FileMetadata::from) + } + + fn canonicalize(&self, path: &Path) -> io::Result { + cfg_if! { + if #[cfg(feature = "yarn_pnp")] { + match VPath::from(path)? { + VPath::Zip(info) => { + dunce::canonicalize(info.physical_base_path().join(info.zip_path)) + } + VPath::Virtual(info) => dunce::canonicalize(info.physical_base_path()), + VPath::Native(path) => dunce::canonicalize(path), + } + } else if #[cfg(windows)] { + dunce::canonicalize(path) + } else { + use std::path::Component; + let mut path_buf = path.to_path_buf(); + loop { + let link = fs::read_link(&path_buf)?; + path_buf.pop(); + for component in link.components() { + match component { + Component::ParentDir => { + path_buf.pop(); + } + Component::Normal(seg) => { + #[cfg(target_family = "wasm")] + // Need to trim the extra \0 introduces by https://github.com/nodejs/uvwasi/issues/262 + { + path_buf.push(seg.to_string_lossy().trim_end_matches('\0')); + } + #[cfg(not(target_family = "wasm"))] + { + path_buf.push(seg); + } + } + Component::RootDir => { + path_buf = PathBuf::from("/"); + } + Component::CurDir | Component::Prefix(_) => {} + } + } + if !fs::symlink_metadata(&path_buf)?.is_symlink() { + break; + } + } + Ok(path_buf) + } + } + } +} + +#[test] +fn metadata() { + let meta = FileMetadata { is_file: true, is_dir: true, is_symlink: true }; + assert_eq!( + format!("{meta:?}"), + "FileMetadata { is_file: true, is_dir: true, is_symlink: true }" + ); + let _ = meta; +} diff --git a/Source/lib.rs b/Source/lib.rs new file mode 100644 index 00000000..e6cb2246 --- /dev/null +++ b/Source/lib.rs @@ -0,0 +1,1755 @@ +//! # Oxc Resolver +//! +//! Node.js [CommonJS][cjs] and [ECMAScript][esm] Module Resolution. +//! +//! Released on [crates.io](https://crates.io/crates/oxc_resolver) and [npm](https://www.npmjs.com/package/oxc-resolver). +//! +//! A module resolution is the process of finding the file referenced by a module specifier in +//! `import "specifier"` or `require("specifier")`. +//! +//! All [configuration options](ResolveOptions) are aligned with webpack's [enhanced-resolve]. +//! +//! ## Terminology +//! +//! ### Specifier +//! +//! For [CommonJS modules][cjs], +//! the specifier is the string passed to the `require` function. e.g. `"id"` in `require("id")`. +//! +//! For [ECMAScript modules][esm], +//! the specifier of an `import` statement is the string after the `from` keyword, +//! e.g. `'specifier'` in `import 'specifier'` or `import { sep } from 'specifier'`. +//! Specifiers are also used in export from statements, and as the argument to an `import()` expression. +//! +//! This is also named "request" in some places. +//! +//! ## References: +//! +//! * Algorithm adapted from Node.js [CommonJS Module Resolution Algorithm] and [ECMAScript Module Resolution Algorithm]. +//! * Tests are ported from [enhanced-resolve]. +//! * Some code is adapted from [parcel-resolver]. +//! * The documentation is copied from [webpack's resolve configuration](https://webpack.js.org/configuration/resolve). +//! +//! [enhanced-resolve]: https://github.com/webpack/enhanced-resolve +//! [CommonJS Module Resolution Algorithm]: https://nodejs.org/api/modules.html#all-together +//! [ECMAScript Module Resolution Algorithm]: https://nodejs.org/api/esm.html#resolution-algorithm-specification +//! [parcel-resolver]: https://github.com/parcel-bundler/parcel/blob/v2/packages/utils/node-resolver-rs +//! [cjs]: https://nodejs.org/api/modules.html +//! [esm]: https://nodejs.org/api/esm.html +//! +//! ## Feature flags +#![cfg_attr(feature = "document-features", doc = document_features::document_features!())] +#![cfg_attr(docsrs, feature(doc_auto_cfg))] +//! +//! ## Example +//! +//! ```rust,ignore +#![doc = include_str!("../examples/resolver.rs")] +//! ``` + +mod builtins; +mod cache; +mod context; +mod error; +mod file_system; +mod options; +mod package_json; +mod path; +mod resolution; +mod specifier; +mod tsconfig; + +#[cfg(test)] +mod tests; + +use std::{ + borrow::Cow, + cmp::Ordering, + ffi::OsStr, + fmt, + path::{Component, Path, PathBuf}, + sync::Arc, +}; + +use rustc_hash::FxHashSet; +use serde_json::Value as JSONValue; + +pub use crate::{ + builtins::NODEJS_BUILTINS, + error::{JSONError, ResolveError, SpecifierError}, + file_system::{FileMetadata, FileSystem}, + options::{ + Alias, AliasValue, EnforceExtension, ResolveOptions, Restriction, TsconfigOptions, + TsconfigReferences, + }, + package_json::PackageJson, + resolution::Resolution, +}; +use crate::{ + cache::{Cache, CachedPath}, + context::ResolveContext as Ctx, + file_system::FileSystemOs, + package_json::JSONMap, + path::{PathUtil, SLASH_START}, + specifier::Specifier, + tsconfig::ExtendsField, + tsconfig::{ProjectReference, TsConfig}, +}; + +type ResolveResult = Result, ResolveError>; + +/// Context returned from the [Resolver::resolve_with_context] API +#[derive(Debug, Default, Clone)] +pub struct ResolveContext { + /// Files that was found on file system + pub file_dependencies: FxHashSet, + + /// Dependencies that was not found on file system + pub missing_dependencies: FxHashSet, +} + +/// Resolver with the current operating system as the file system +pub type Resolver = ResolverGeneric; + +/// Generic implementation of the resolver, can be configured by the [FileSystem] trait +pub struct ResolverGeneric { + options: ResolveOptions, + cache: Arc>, +} + +impl fmt::Debug for ResolverGeneric { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.options.fmt(f) + } +} + +impl Default for ResolverGeneric { + fn default() -> Self { + Self::new(ResolveOptions::default()) + } +} + +impl ResolverGeneric { + pub fn new(options: ResolveOptions) -> Self { + Self { options: options.sanitize(), cache: Arc::new(Cache::new(Fs::default())) } + } +} + +impl ResolverGeneric { + pub fn new_with_file_system(file_system: Fs, options: ResolveOptions) -> Self { + Self { cache: Arc::new(Cache::new(file_system)), options: options.sanitize() } + } + + /// Clone the resolver using the same underlying cache. + #[must_use] + pub fn clone_with_options(&self, options: ResolveOptions) -> Self { + Self { options: options.sanitize(), cache: Arc::clone(&self.cache) } + } + + /// Returns the options. + pub fn options(&self) -> &ResolveOptions { + &self.options + } + + /// Clear the underlying cache. + pub fn clear_cache(&self) { + self.cache.clear(); + } + + /// Resolve `specifier` at an absolute path to a `directory`. + /// + /// A specifier is the string passed to require or import, i.e. `require("specifier")` or `import "specifier"`. + /// + /// `directory` must be an **absolute** path to a directory where the specifier is resolved against. + /// For CommonJS modules, it is the `__dirname` variable that contains the absolute path to the folder containing current module. + /// For ECMAScript modules, it is the value of `import.meta.url`. + /// + /// # Errors + /// + /// * See [ResolveError] + pub fn resolve>( + &self, + directory: P, + specifier: &str, + ) -> Result { + let mut ctx = Ctx::default(); + self.resolve_tracing(directory.as_ref(), specifier, &mut ctx) + } + + /// Resolve `specifier` at absolute `path` with [ResolveContext] + /// + /// # Errors + /// + /// * See [ResolveError] + pub fn resolve_with_context>( + &self, + directory: P, + specifier: &str, + resolve_context: &mut ResolveContext, + ) -> Result { + let mut ctx = Ctx::default(); + ctx.init_file_dependencies(); + let result = self.resolve_tracing(directory.as_ref(), specifier, &mut ctx); + if let Some(deps) = &mut ctx.file_dependencies { + resolve_context.file_dependencies.extend(deps.drain(..)); + } + if let Some(deps) = &mut ctx.missing_dependencies { + resolve_context.missing_dependencies.extend(deps.drain(..)); + } + result + } + + /// Wrap `resolve_impl` with `tracing` information + fn resolve_tracing( + &self, + directory: &Path, + specifier: &str, + ctx: &mut Ctx, + ) -> Result { + let span = tracing::debug_span!("resolve", path = ?directory, specifier = specifier); + let _enter = span.enter(); + let r = self.resolve_impl(directory, specifier, ctx); + match &r { + Ok(r) => { + tracing::debug!(options = ?self.options, path = ?directory, specifier = specifier, ret = ?r.path); + } + Err(err) => { + tracing::debug!(options = ?self.options, path = ?directory, specifier = specifier, err = ?err); + } + }; + r + } + + fn resolve_impl( + &self, + path: &Path, + specifier: &str, + ctx: &mut Ctx, + ) -> Result { + ctx.with_fully_specified(self.options.fully_specified); + let cached_path = self.cache.value(path); + let cached_path = self.require(&cached_path, specifier, ctx)?; + let path = self.load_realpath(&cached_path)?; + // enhanced-resolve: restrictions + self.check_restrictions(&path)?; + let package_json = cached_path.find_package_json(&self.cache.fs, &self.options, ctx)?; + if let Some(package_json) = &package_json { + // path must be inside the package. + debug_assert!(path.starts_with(package_json.directory())); + } + Ok(Resolution { + path, + query: ctx.query.take(), + fragment: ctx.fragment.take(), + package_json, + }) + } + + /// require(X) from module at path Y + /// + /// X: specifier + /// Y: path + /// + /// + fn require( + &self, + cached_path: &CachedPath, + specifier: &str, + ctx: &mut Ctx, + ) -> Result { + ctx.test_for_infinite_recursion()?; + + // enhanced-resolve: parse + let (parsed, try_fragment_as_path) = self.load_parse(cached_path, specifier, ctx)?; + if let Some(path) = try_fragment_as_path { + return Ok(path); + } + + self.require_without_parse(cached_path, parsed.path(), ctx) + } + + fn require_without_parse( + &self, + cached_path: &CachedPath, + specifier: &str, + ctx: &mut Ctx, + ) -> Result { + // tsconfig-paths + if let Some(path) = self.load_tsconfig_paths(cached_path, specifier, &mut Ctx::default())? { + return Ok(path); + } + + // enhanced-resolve: try alias + if let Some(path) = self.load_alias(cached_path, specifier, &self.options.alias, ctx)? { + return Ok(path); + } + + let result = match Path::new(specifier).components().next() { + // 2. If X begins with '/' + Some(Component::RootDir | Component::Prefix(_)) => { + self.require_absolute(cached_path, specifier, ctx) + } + // 3. If X begins with './' or '/' or '../' + Some(Component::CurDir | Component::ParentDir) => { + self.require_relative(cached_path, specifier, ctx) + } + // 4. If X begins with '#' + Some(Component::Normal(_)) if specifier.as_bytes()[0] == b'#' => { + self.require_hash(cached_path, specifier, ctx) + } + _ => { + // 1. If X is a core module, + // a. return the core module + // b. STOP + self.require_core(specifier)?; + + // (ESM) 5. Otherwise, + // Note: specifier is now a bare specifier. + // Set resolved the result of PACKAGE_RESOLVE(specifier, parentURL). + self.require_bare(cached_path, specifier, ctx) + } + }; + + result.or_else(|err| { + if err.is_ignore() { + return Err(err); + } + // enhanced-resolve: try fallback + self.load_alias(cached_path, specifier, &self.options.fallback, ctx) + .and_then(|value| value.ok_or(err)) + }) + } + + // PACKAGE_RESOLVE(packageSpecifier, parentURL) + // 3. If packageSpecifier is a Node.js builtin module name, then + // 1. Return the string "node:" concatenated with packageSpecifier. + fn require_core(&self, specifier: &str) -> Result<(), ResolveError> { + if self.options.builtin_modules { + let starts_with_node = specifier.starts_with("node:"); + if starts_with_node || NODEJS_BUILTINS.binary_search(&specifier).is_ok() { + let mut specifier = specifier.to_string(); + if !starts_with_node { + specifier = format!("node:{specifier}"); + } + return Err(ResolveError::Builtin(specifier)); + } + } + Ok(()) + } + + fn require_absolute( + &self, + cached_path: &CachedPath, + specifier: &str, + ctx: &mut Ctx, + ) -> Result { + // Make sure only path prefixes gets called + debug_assert!(Path::new(specifier) + .components() + .next() + .is_some_and(|c| matches!(c, Component::RootDir | Component::Prefix(_)))); + if !self.options.prefer_relative && self.options.prefer_absolute { + if let Ok(path) = self.load_package_self_or_node_modules(cached_path, specifier, ctx) { + return Ok(path); + } + } + if let Some(path) = self.load_roots(specifier, ctx) { + return Ok(path); + } + // 2. If X begins with '/' + // a. set Y to be the file system root + let path = self.cache.value(Path::new(specifier)); + if let Some(path) = self.load_as_file_or_directory(&path, specifier, ctx)? { + return Ok(path); + } + Err(ResolveError::NotFound(specifier.to_string())) + } + + // 3. If X begins with './' or '/' or '../' + fn require_relative( + &self, + cached_path: &CachedPath, + specifier: &str, + ctx: &mut Ctx, + ) -> Result { + // Make sure only relative or normal paths gets called + debug_assert!(Path::new(specifier).components().next().is_some_and(|c| matches!( + c, + Component::CurDir | Component::ParentDir | Component::Normal(_) + ))); + let path = cached_path.path().normalize_with(specifier); + let cached_path = self.cache.value(&path); + // a. LOAD_AS_FILE(Y + X) + // b. LOAD_AS_DIRECTORY(Y + X) + if let Some(path) = self.load_as_file_or_directory(&cached_path, specifier, ctx)? { + return Ok(path); + } + // c. THROW "not found" + Err(ResolveError::NotFound(specifier.to_string())) + } + + fn require_hash( + &self, + cached_path: &CachedPath, + specifier: &str, + ctx: &mut Ctx, + ) -> Result { + debug_assert_eq!(specifier.chars().next(), Some('#')); + // a. LOAD_PACKAGE_IMPORTS(X, dirname(Y)) + if let Some(path) = self.load_package_imports(cached_path, specifier, ctx)? { + return Ok(path); + } + self.load_package_self_or_node_modules(cached_path, specifier, ctx) + } + + fn require_bare( + &self, + cached_path: &CachedPath, + specifier: &str, + ctx: &mut Ctx, + ) -> Result { + // Make sure no other path prefixes gets called + debug_assert!(Path::new(specifier) + .components() + .next() + .is_some_and(|c| matches!(c, Component::Normal(_)))); + if self.options.prefer_relative { + if let Ok(path) = self.require_relative(cached_path, specifier, ctx) { + return Ok(path); + } + } + self.load_package_self_or_node_modules(cached_path, specifier, ctx) + } + + /// enhanced-resolve: ParsePlugin. + /// + /// It's allowed to escape # as \0# to avoid parsing it as fragment. + /// enhanced-resolve will try to resolve requests containing `#` as path and as fragment, + /// so it will automatically figure out if `./some#thing` means `.../some.js#thing` or `.../some#thing.js`. + /// When a # is resolved as path it will be escaped in the result. Here: `.../some\0#thing.js`. + /// + /// + fn load_parse<'s>( + &self, + cached_path: &CachedPath, + specifier: &'s str, + ctx: &mut Ctx, + ) -> Result<(Specifier<'s>, Option), ResolveError> { + let parsed = Specifier::parse(specifier).map_err(ResolveError::Specifier)?; + ctx.with_query_fragment(parsed.query, parsed.fragment); + + // There is an edge-case where a request with # can be a path or a fragment -> try both + if ctx.fragment.is_some() && ctx.query.is_none() { + let specifier = parsed.path(); + let fragment = ctx.fragment.take().unwrap(); + let path = format!("{specifier}{fragment}"); + if let Ok(path) = self.require_without_parse(cached_path, &path, ctx) { + return Ok((parsed, Some(path))); + } + ctx.fragment.replace(fragment); + } + Ok((parsed, None)) + } + + fn load_package_self_or_node_modules( + &self, + cached_path: &CachedPath, + specifier: &str, + ctx: &mut Ctx, + ) -> Result { + let (_, subpath) = Self::parse_package_specifier(specifier); + if subpath.is_empty() { + ctx.with_fully_specified(false); + } + // 5. LOAD_PACKAGE_SELF(X, dirname(Y)) + if let Some(path) = self.load_package_self(cached_path, specifier, ctx)? { + return Ok(path); + } + // 6. LOAD_NODE_MODULES(X, dirname(Y)) + if let Some(path) = self.load_node_modules(cached_path, specifier, ctx)? { + return Ok(path); + } + // 7. THROW "not found" + Err(ResolveError::NotFound(specifier.to_string())) + } + + /// LOAD_PACKAGE_IMPORTS(X, DIR) + fn load_package_imports( + &self, + cached_path: &CachedPath, + specifier: &str, + ctx: &mut Ctx, + ) -> ResolveResult { + // 1. Find the closest package scope SCOPE to DIR. + // 2. If no scope was found, return. + let Some(package_json) = + cached_path.find_package_json(&self.cache.fs, &self.options, ctx)? + else { + return Ok(None); + }; + // 3. If the SCOPE/package.json "imports" is null or undefined, return. + // 4. let MATCH = PACKAGE_IMPORTS_RESOLVE(X, pathToFileURL(SCOPE), ["node", "require"]) defined in the ESM resolver. + if let Some(path) = self.package_imports_resolve(specifier, &package_json, ctx)? { + // 5. RESOLVE_ESM_MATCH(MATCH). + return self.resolve_esm_match(specifier, &path, ctx); + } + Ok(None) + } + + fn load_as_file(&self, cached_path: &CachedPath, ctx: &mut Ctx) -> ResolveResult { + // enhanced-resolve feature: extension_alias + if let Some(path) = self.load_extension_alias(cached_path, ctx)? { + return Ok(Some(path)); + } + if self.options.enforce_extension.is_disabled() { + // 1. If X is a file, load X as its file extension format. STOP + if let Some(path) = self.load_alias_or_file(cached_path, ctx)? { + return Ok(Some(path)); + } + } + // 2. If X.js is a file, load X.js as JavaScript text. STOP + // 3. If X.json is a file, parse X.json to a JavaScript Object. STOP + // 4. If X.node is a file, load X.node as binary addon. STOP + if let Some(path) = self.load_extensions(cached_path, &self.options.extensions, ctx)? { + return Ok(Some(path)); + } + Ok(None) + } + + fn load_as_directory(&self, cached_path: &CachedPath, ctx: &mut Ctx) -> ResolveResult { + // TODO: Only package.json is supported, so warn about having other values + // Checking for empty files is needed for omitting checks on package.json + // 1. If X/package.json is a file, + if !self.options.description_files.is_empty() { + // a. Parse X/package.json, and look for "main" field. + if let Some(package_json) = + cached_path.package_json(&self.cache.fs, &self.options, ctx)? + { + // b. If "main" is a falsy value, GOTO 2. + for main_field in package_json.main_fields(&self.options.main_fields) { + // c. let M = X + (json main field) + let main_field_path = cached_path.path().normalize_with(main_field); + // d. LOAD_AS_FILE(M) + let cached_path = self.cache.value(&main_field_path); + if let Some(path) = self.load_as_file(&cached_path, ctx)? { + return Ok(Some(path)); + } + // e. LOAD_INDEX(M) + if let Some(path) = self.load_index(&cached_path, ctx)? { + return Ok(Some(path)); + } + } + // f. LOAD_INDEX(X) DEPRECATED + // g. THROW "not found" + } + } + // 2. LOAD_INDEX(X) + self.load_index(cached_path, ctx) + } + + fn load_as_file_or_directory( + &self, + cached_path: &CachedPath, + specifier: &str, + ctx: &mut Ctx, + ) -> ResolveResult { + if self.options.resolve_to_context { + return Ok(cached_path.is_dir(&self.cache.fs, ctx).then(|| cached_path.clone())); + } + if !specifier.ends_with('/') { + if let Some(path) = self.load_as_file(cached_path, ctx)? { + return Ok(Some(path)); + } + } + if cached_path.is_dir(&self.cache.fs, ctx) { + if let Some(path) = self.load_as_directory(cached_path, ctx)? { + return Ok(Some(path)); + } + } + Ok(None) + } + + fn load_extensions( + &self, + path: &CachedPath, + extensions: &[String], + ctx: &mut Ctx, + ) -> ResolveResult { + if ctx.fully_specified { + return Ok(None); + } + let path = path.path().as_os_str(); + for extension in extensions { + let mut path_with_extension = path.to_os_string(); + path_with_extension.reserve_exact(extension.len()); + path_with_extension.push(extension); + let cached_path = self.cache.value(Path::new(&path_with_extension)); + if let Some(path) = self.load_alias_or_file(&cached_path, ctx)? { + return Ok(Some(path)); + } + } + Ok(None) + } + + fn load_realpath(&self, cached_path: &CachedPath) -> Result { + if self.options.symlinks { + cached_path.realpath(&self.cache.fs).map_err(ResolveError::from) + } else { + Ok(cached_path.to_path_buf()) + } + } + + fn check_restrictions(&self, path: &Path) -> Result<(), ResolveError> { + // https://github.com/webpack/enhanced-resolve/blob/a998c7d218b7a9ec2461fc4fddd1ad5dd7687485/lib/RestrictionsPlugin.js#L19-L24 + fn is_inside(path: &Path, parent: &Path) -> bool { + if !path.starts_with(parent) { + return false; + } + if path.as_os_str().len() == parent.as_os_str().len() { + return true; + } + path.strip_prefix(parent).is_ok_and(|p| p == Path::new("./")) + } + for restriction in &self.options.restrictions { + match restriction { + Restriction::Path(restricted_path) => { + if !is_inside(path, restricted_path) { + return Err(ResolveError::Restriction( + path.to_path_buf(), + restricted_path.clone(), + )); + } + } + Restriction::RegExp(_) => { + return Err(ResolveError::Unimplemented("Restriction with regex")) + } + } + } + Ok(()) + } + + fn load_index(&self, cached_path: &CachedPath, ctx: &mut Ctx) -> ResolveResult { + for main_file in &self.options.main_files { + let main_path = cached_path.path().normalize_with(main_file); + let cached_path = self.cache.value(&main_path); + if self.options.enforce_extension.is_disabled() { + if let Some(path) = self.load_alias_or_file(&cached_path, ctx)? { + return Ok(Some(path)); + } + } + // 1. If X/index.js is a file, load X/index.js as JavaScript text. STOP + // 2. If X/index.json is a file, parse X/index.json to a JavaScript object. STOP + // 3. If X/index.node is a file, load X/index.node as binary addon. STOP + if let Some(path) = self.load_extensions(&cached_path, &self.options.extensions, ctx)? { + return Ok(Some(path)); + } + } + Ok(None) + } + + fn load_alias_or_file(&self, cached_path: &CachedPath, ctx: &mut Ctx) -> ResolveResult { + if !self.options.alias_fields.is_empty() { + if let Some(package_json) = + cached_path.find_package_json(&self.cache.fs, &self.options, ctx)? + { + if let Some(path) = + self.load_browser_field(cached_path, None, &package_json, ctx)? + { + return Ok(Some(path)); + } + } + } + // enhanced-resolve: try file as alias + let alias_specifier = cached_path.path().to_string_lossy(); + if let Some(path) = + self.load_alias(cached_path, &alias_specifier, &self.options.alias, ctx)? + { + return Ok(Some(path)); + } + if cached_path.is_file(&self.cache.fs, ctx) { + return Ok(Some(cached_path.clone())); + } + Ok(None) + } + + fn load_node_modules( + &self, + cached_path: &CachedPath, + specifier: &str, + ctx: &mut Ctx, + ) -> ResolveResult { + #[cfg(feature = "yarn_pnp")] + { + if let Some(resolved_path) = self.load_pnp(cached_path, specifier, ctx)? { + return Ok(Some(resolved_path)); + } + } + + let (package_name, subpath) = Self::parse_package_specifier(specifier); + // 1. let DIRS = NODE_MODULES_PATHS(START) + // 2. for each DIR in DIRS: + for module_name in &self.options.modules { + for cached_path in std::iter::successors(Some(cached_path), |p| p.parent()) { + // Skip if /path/to/node_modules does not exist + if !cached_path.is_dir(&self.cache.fs, ctx) { + continue; + } + + let Some(cached_path) = self.get_module_directory(cached_path, module_name, ctx) + else { + continue; + }; + // Optimize node_modules lookup by inspecting whether the package exists + // From LOAD_PACKAGE_EXPORTS(X, DIR) + // 1. Try to interpret X as a combination of NAME and SUBPATH where the name + // may have a @scope/ prefix and the subpath begins with a slash (`/`). + if !package_name.is_empty() { + let package_path = cached_path.path().normalize_with(package_name); + let cached_path = self.cache.value(&package_path); + // Try foo/node_modules/package_name + if cached_path.is_dir(&self.cache.fs, ctx) { + // a. LOAD_PACKAGE_EXPORTS(X, DIR) + if let Some(path) = + self.load_package_exports(specifier, subpath, &cached_path, ctx)? + { + return Ok(Some(path)); + } + } else { + // foo/node_modules/package_name is not a directory, so useless to check inside it + if !subpath.is_empty() { + continue; + } + // Skip if the directory lead to the scope package does not exist + // i.e. `foo/node_modules/@scope` is not a directory for `foo/node_modules/@scope/package` + if package_name.starts_with('@') { + if let Some(path) = cached_path.parent() { + if !path.is_dir(&self.cache.fs, ctx) { + continue; + } + } + } + } + } + + // Try as file or directory for all other cases + // b. LOAD_AS_FILE(DIR/X) + // c. LOAD_AS_DIRECTORY(DIR/X) + let node_module_file = cached_path.path().normalize_with(specifier); + let cached_path = self.cache.value(&node_module_file); + if let Some(path) = self.load_as_file_or_directory(&cached_path, specifier, ctx)? { + return Ok(Some(path)); + } + } + } + Ok(None) + } + + #[cfg(feature = "yarn_pnp")] + fn load_pnp( + &self, + cached_path: &CachedPath, + specifier: &str, + ctx: &mut Ctx, + ) -> Result, ResolveError> { + let Some(pnp_manifest) = &self.options.pnp_manifest else { return Ok(None) }; + let resolution = + pnp::resolve_to_unqualified_via_manifest(pnp_manifest, specifier, cached_path.path()); + match resolution { + Ok(pnp::Resolution::Resolved(path, subpath)) => { + let cached_path = self.cache.value(&path); + let export_resolution = self.load_package_exports( + specifier, + &subpath.unwrap_or_default(), + &cached_path, + ctx, + )?; + if export_resolution.is_some() { + return Ok(export_resolution); + } + let file_or_directory_resolution = + self.load_as_file_or_directory(&cached_path, specifier, ctx)?; + if file_or_directory_resolution.is_some() { + return Ok(file_or_directory_resolution); + } + Err(ResolveError::NotFound(specifier.to_string())) + } + + Ok(pnp::Resolution::Skipped) => Ok(None), + + Err(_) => { + // Todo: Add a ResolveError::Pnp variant? + Err(ResolveError::NotFound(specifier.to_string())) + } + } + } + + fn get_module_directory( + &self, + cached_path: &CachedPath, + module_name: &str, + ctx: &mut Ctx, + ) -> Option { + if module_name == "node_modules" { + cached_path.cached_node_modules(&self.cache, ctx) + } else if cached_path.path().components().next_back() + == Some(Component::Normal(OsStr::new(module_name))) + { + Some(cached_path.clone()) + } else { + cached_path.module_directory(module_name, &self.cache, ctx) + } + } + + fn load_package_exports( + &self, + specifier: &str, + subpath: &str, + cached_path: &CachedPath, + ctx: &mut Ctx, + ) -> ResolveResult { + // 2. If X does not match this pattern or DIR/NAME/package.json is not a file, + // return. + let Some(package_json) = cached_path.package_json(&self.cache.fs, &self.options, ctx)? + else { + return Ok(None); + }; + // 3. Parse DIR/NAME/package.json, and look for "exports" field. + // 4. If "exports" is null or undefined, return. + // 5. let MATCH = PACKAGE_EXPORTS_RESOLVE(pathToFileURL(DIR/NAME), "." + SUBPATH, + // `package.json` "exports", ["node", "require"]) defined in the ESM resolver. + // Note: The subpath is not prepended with a dot on purpose + for exports in package_json.exports_fields(&self.options.exports_fields) { + if let Some(path) = self.package_exports_resolve( + cached_path.path(), + &format!(".{subpath}"), + exports, + ctx, + )? { + // 6. RESOLVE_ESM_MATCH(MATCH) + return self.resolve_esm_match(specifier, &path, ctx); + }; + } + Ok(None) + } + + fn load_package_self( + &self, + cached_path: &CachedPath, + specifier: &str, + ctx: &mut Ctx, + ) -> ResolveResult { + // 1. Find the closest package scope SCOPE to DIR. + // 2. If no scope was found, return. + let Some(package_json) = + cached_path.find_package_json(&self.cache.fs, &self.options, ctx)? + else { + return Ok(None); + }; + // 3. If the SCOPE/package.json "exports" is null or undefined, return. + // 4. If the SCOPE/package.json "name" is not the first segment of X, return. + if let Some(subpath) = package_json + .name + .as_ref() + .and_then(|package_name| Self::strip_package_name(specifier, package_name)) + { + // 5. let MATCH = PACKAGE_EXPORTS_RESOLVE(pathToFileURL(SCOPE), + // "." + X.slice("name".length), `package.json` "exports", ["node", "require"]) + // defined in the ESM resolver. + let package_url = package_json.directory(); + // Note: The subpath is not prepended with a dot on purpose + // because `package_exports_resolve` matches subpath without the leading dot. + for exports in package_json.exports_fields(&self.options.exports_fields) { + if let Some(cached_path) = + self.package_exports_resolve(package_url, &format!(".{subpath}"), exports, ctx)? + { + // 6. RESOLVE_ESM_MATCH(MATCH) + return self.resolve_esm_match(specifier, &cached_path, ctx); + } + } + } + self.load_browser_field(cached_path, Some(specifier), &package_json, ctx) + } + + /// RESOLVE_ESM_MATCH(MATCH) + fn resolve_esm_match( + &self, + specifier: &str, + cached_path: &CachedPath, + ctx: &mut Ctx, + ) -> ResolveResult { + // 1. let RESOLVED_PATH = fileURLToPath(MATCH) + // 2. If the file at RESOLVED_PATH exists, load RESOLVED_PATH as its extension format. STOP + // + // Non-compliant ESM can result in a directory, so directory is tried as well. + if let Some(path) = self.load_as_file_or_directory(cached_path, "", ctx)? { + return Ok(Some(path)); + } + // 3. THROW "not found" + Err(ResolveError::NotFound(specifier.to_string())) + } + + /// enhanced-resolve: AliasFieldPlugin for [ResolveOptions::alias_fields] + fn load_browser_field( + &self, + cached_path: &CachedPath, + module_specifier: Option<&str>, + package_json: &PackageJson, + ctx: &mut Ctx, + ) -> ResolveResult { + let path = cached_path.path(); + let Some(new_specifier) = package_json.resolve_browser_field( + path, + module_specifier, + &self.options.alias_fields, + )? + else { + return Ok(None); + }; + // Abort when resolving recursive module + if module_specifier.is_some_and(|s| s == new_specifier) { + return Ok(None); + } + if ctx.resolving_alias.as_ref().is_some_and(|s| s == new_specifier) { + // Complete when resolving to self `{"./a.js": "./a.js"}` + if new_specifier.strip_prefix("./").filter(|s| path.ends_with(Path::new(s))).is_some() { + return if cached_path.is_file(&self.cache.fs, ctx) { + Ok(Some(cached_path.clone())) + } else { + Err(ResolveError::NotFound(new_specifier.to_string())) + }; + } + return Err(ResolveError::Recursion); + } + ctx.with_resolving_alias(new_specifier.to_string()); + ctx.with_fully_specified(false); + let cached_path = self.cache.value(package_json.directory()); + self.require(&cached_path, new_specifier, ctx).map(Some) + } + + /// enhanced-resolve: AliasPlugin for [ResolveOptions::alias] and [ResolveOptions::fallback]. + fn load_alias( + &self, + cached_path: &CachedPath, + specifier: &str, + aliases: &Alias, + ctx: &mut Ctx, + ) -> ResolveResult { + for (alias_key_raw, specifiers) in aliases { + let alias_key = if let Some(alias_key) = alias_key_raw.strip_suffix('$') { + if alias_key != specifier { + continue; + } + alias_key + } else { + let strip_package_name = Self::strip_package_name(specifier, alias_key_raw); + if strip_package_name.is_none() { + continue; + } + alias_key_raw + }; + // It should stop resolving when all of the tried alias values + // failed to resolve. + // + let mut should_stop = false; + for r in specifiers { + match r { + AliasValue::Path(alias_value) => { + if let Some(path) = self.load_alias_value( + cached_path, + alias_key, + alias_value, + specifier, + ctx, + &mut should_stop, + )? { + return Ok(Some(path)); + } + } + AliasValue::Ignore => { + let path = cached_path.path().normalize_with(alias_key); + return Err(ResolveError::Ignored(path)); + } + } + } + if should_stop { + return Err(ResolveError::MatchedAliasNotFound( + specifier.to_string(), + alias_key.to_string(), + )); + } + } + Ok(None) + } + + fn load_alias_value( + &self, + cached_path: &CachedPath, + alias_key: &str, + alias_value: &str, + request: &str, + ctx: &mut Ctx, + should_stop: &mut bool, + ) -> ResolveResult { + if request != alias_value + && !request.strip_prefix(alias_value).is_some_and(|prefix| prefix.starts_with('/')) + { + let tail = &request[alias_key.len()..]; + + let new_specifier = if tail.is_empty() { + Cow::Borrowed(alias_value) + } else { + let alias_value = Path::new(alias_value).normalize(); + // Must not append anything to alias_value if it is a file. + let alias_value_cached_path = self.cache.value(&alias_value); + if alias_value_cached_path.is_file(&self.cache.fs, ctx) { + return Ok(None); + } + + // Remove the leading slash so the final path is concatenated. + let tail = tail.trim_start_matches(SLASH_START); + let normalized = alias_value.normalize_with(tail); + Cow::Owned(normalized.to_string_lossy().to_string()) + }; + + *should_stop = true; + ctx.with_fully_specified(false); + return match self.require(cached_path, new_specifier.as_ref(), ctx) { + Err(ResolveError::NotFound(_) | ResolveError::MatchedAliasNotFound(_, _)) => { + Ok(None) + } + Ok(path) => return Ok(Some(path)), + Err(err) => return Err(err), + }; + } + Ok(None) + } + + /// Given an extension alias map `{".js": [".ts", ".js"]}`, + /// load the mapping instead of the provided extension + /// + /// This is an enhanced-resolve feature + /// + /// # Errors + /// + /// * [ResolveError::ExtensionAlias]: When all of the aliased extensions are not found + fn load_extension_alias(&self, cached_path: &CachedPath, ctx: &mut Ctx) -> ResolveResult { + if self.options.extension_alias.is_empty() { + return Ok(None); + } + let Some(path_extension) = cached_path.path().extension() else { + return Ok(None); + }; + let Some((_, extensions)) = self + .options + .extension_alias + .iter() + .find(|(ext, _)| OsStr::new(ext.trim_start_matches('.')) == path_extension) + else { + return Ok(None); + }; + let path = cached_path.path().with_extension(""); + let path = path.as_os_str(); + ctx.with_fully_specified(true); + for extension in extensions { + let mut path_with_extension = path.to_os_string(); + path_with_extension.reserve_exact(extension.len()); + path_with_extension.push(extension); + let cached_path = self.cache.value(Path::new(&path_with_extension)); + // Bail if path is module directory such as `ipaddr.js` + if cached_path.is_dir(&self.cache.fs, ctx) { + ctx.with_fully_specified(false); + return Ok(None); + } + if let Some(path) = self.load_alias_or_file(&cached_path, ctx)? { + ctx.with_fully_specified(false); + return Ok(Some(path)); + } + } + Err(ResolveError::ExtensionAlias(cached_path.to_path_buf())) + } + + /// enhanced-resolve: RootsPlugin + /// + /// A list of directories where requests of server-relative URLs (starting with '/') are resolved, + /// defaults to context configuration option. + /// + /// On non-Windows systems these requests are resolved as an absolute path first. + fn load_roots(&self, specifier: &str, ctx: &mut Ctx) -> Option { + if self.options.roots.is_empty() { + return None; + } + if let Some(specifier) = specifier.strip_prefix(SLASH_START) { + for root in &self.options.roots { + let cached_path = self.cache.value(root); + if let Ok(path) = self.require_relative(&cached_path, specifier, ctx) { + return Some(path); + } + } + } + None + } + + fn load_tsconfig_paths( + &self, + cached_path: &CachedPath, + specifier: &str, + ctx: &mut Ctx, + ) -> ResolveResult { + let Some(tsconfig_options) = &self.options.tsconfig else { + return Ok(None); + }; + let tsconfig = self.load_tsconfig( + /* root */ true, + &tsconfig_options.config_file, + &tsconfig_options.references, + )?; + let paths = tsconfig.resolve(cached_path.path(), specifier); + for path in paths { + let cached_path = self.cache.value(&path); + if let Ok(path) = self.require_relative(&cached_path, ".", ctx) { + return Ok(Some(path)); + } + } + Ok(None) + } + + fn load_tsconfig( + &self, + root: bool, + path: &Path, + references: &TsconfigReferences, + ) -> Result, ResolveError> { + self.cache.tsconfig(root, path, |tsconfig| { + let directory = self.cache.value(tsconfig.directory()); + tracing::trace!(tsconfig = ?tsconfig, "load_tsconfig"); + + // Extend tsconfig + if let Some(extends) = &tsconfig.extends { + let extended_tsconfig_paths = match extends { + ExtendsField::Single(s) => { + vec![self.get_extended_tsconfig_path(&directory, tsconfig, s)?] + } + ExtendsField::Multiple(specifiers) => specifiers + .iter() + .map(|s| self.get_extended_tsconfig_path(&directory, tsconfig, s)) + .collect::, ResolveError>>()?, + }; + for extended_tsconfig_path in extended_tsconfig_paths { + let extended_tsconfig = self.load_tsconfig( + /* root */ false, + &extended_tsconfig_path, + &TsconfigReferences::Disabled, + )?; + tsconfig.extend_tsconfig(&extended_tsconfig); + } + } + + // Load project references + match references { + TsconfigReferences::Disabled => { + tsconfig.references.drain(..); + } + TsconfigReferences::Auto => {} + TsconfigReferences::Paths(paths) => { + tsconfig.references = paths + .iter() + .map(|path| ProjectReference { path: path.clone(), tsconfig: None }) + .collect(); + } + } + if !tsconfig.references.is_empty() { + let directory = tsconfig.directory().to_path_buf(); + for reference in &mut tsconfig.references { + let reference_tsconfig_path = directory.normalize_with(&reference.path); + let tsconfig = self.cache.tsconfig( + /* root */ true, + &reference_tsconfig_path, + |reference_tsconfig| { + if reference_tsconfig.path == tsconfig.path { + return Err(ResolveError::TsconfigSelfReference( + reference_tsconfig.path.clone(), + )); + } + Ok(()) + }, + )?; + reference.tsconfig.replace(tsconfig); + } + } + Ok(()) + }) + } + + fn get_extended_tsconfig_path( + &self, + directory: &CachedPath, + tsconfig: &TsConfig, + specifier: &str, + ) -> Result { + match specifier.as_bytes().first() { + None => Err(ResolveError::Specifier(SpecifierError::Empty(specifier.to_string()))), + Some(b'/') => Ok(PathBuf::from(specifier)), + Some(b'.') => Ok(tsconfig.directory().normalize_with(specifier)), + _ => self + .clone_with_options(ResolveOptions { + description_files: vec![], + extensions: vec![".json".into()], + main_files: vec!["tsconfig.json".into()], + ..ResolveOptions::default() + }) + .load_package_self_or_node_modules(directory, specifier, &mut Ctx::default()) + .map(|p| p.to_path_buf()) + .map_err(|err| match err { + ResolveError::NotFound(_) => { + ResolveError::TsconfigNotFound(PathBuf::from(specifier)) + } + _ => err, + }), + } + } + + /// PACKAGE_RESOLVE(packageSpecifier, parentURL) + fn package_resolve( + &self, + cached_path: &CachedPath, + specifier: &str, + ctx: &mut Ctx, + ) -> ResolveResult { + let (package_name, subpath) = Self::parse_package_specifier(specifier); + + // 3. If packageSpecifier is a Node.js builtin module name, then + // 1. Return the string "node:" concatenated with packageSpecifier. + self.require_core(package_name)?; + + // 11. While parentURL is not the file system root, + for module_name in &self.options.modules { + for cached_path in std::iter::successors(Some(cached_path), |p| p.parent()) { + // 1. Let packageURL be the URL resolution of "node_modules/" concatenated with packageSpecifier, relative to parentURL. + let Some(cached_path) = self.get_module_directory(cached_path, module_name, ctx) + else { + continue; + }; + // 2. Set parentURL to the parent folder URL of parentURL. + let package_path = cached_path.path().normalize_with(package_name); + let cached_path = self.cache.value(&package_path); + // 3. If the folder at packageURL does not exist, then + // 1. Continue the next loop iteration. + if cached_path.is_dir(&self.cache.fs, ctx) { + // 4. Let pjson be the result of READ_PACKAGE_JSON(packageURL). + if let Some(package_json) = + cached_path.package_json(&self.cache.fs, &self.options, ctx)? + { + // 5. If pjson is not null and pjson.exports is not null or undefined, then + // 1. Return the result of PACKAGE_EXPORTS_RESOLVE(packageURL, packageSubpath, pjson.exports, defaultConditions). + for exports in package_json.exports_fields(&self.options.exports_fields) { + if let Some(path) = self.package_exports_resolve( + cached_path.path(), + &format!(".{subpath}"), + exports, + ctx, + )? { + return Ok(Some(path)); + } + } + // 6. Otherwise, if packageSubpath is equal to ".", then + if subpath == "." { + // 1. If pjson.main is a string, then + for main_field in package_json.main_fields(&self.options.main_fields) { + // 1. Return the URL resolution of main in packageURL. + let path = cached_path.path().normalize_with(main_field); + let cached_path = self.cache.value(&path); + if cached_path.is_file(&self.cache.fs, ctx) { + return Ok(Some(cached_path)); + } + } + } + } + let subpath = format!(".{subpath}"); + ctx.with_fully_specified(false); + return self.require(&cached_path, &subpath, ctx).map(Some); + } + } + } + + Err(ResolveError::NotFound(specifier.to_string())) + } + + /// PACKAGE_EXPORTS_RESOLVE(packageURL, subpath, exports, conditions) + fn package_exports_resolve( + &self, + package_url: &Path, + subpath: &str, + exports: &JSONValue, + ctx: &mut Ctx, + ) -> ResolveResult { + let conditions = &self.options.condition_names; + // 1. If exports is an Object with both a key starting with "." and a key not starting with ".", throw an Invalid Package Configuration error. + if let JSONValue::Object(map) = exports { + let mut has_dot = false; + let mut without_dot = false; + for key in map.keys() { + let starts_with_dot_or_hash = key.starts_with(['.', '#']); + has_dot = has_dot || starts_with_dot_or_hash; + without_dot = without_dot || !starts_with_dot_or_hash; + if has_dot && without_dot { + return Err(ResolveError::InvalidPackageConfig( + package_url.join("package.json"), + )); + } + } + } + // 2. If subpath is equal to ".", then + // Note: subpath is not prepended with a dot when passed in. + if subpath == "." { + // enhanced-resolve appends query and fragment when resolving exports field + // https://github.com/webpack/enhanced-resolve/blob/a998c7d218b7a9ec2461fc4fddd1ad5dd7687485/lib/ExportsFieldPlugin.js#L57-L62 + // This is only need when querying the main export, otherwise ctx is passed through. + if ctx.query.is_some() || ctx.fragment.is_some() { + let query = ctx.query.clone().unwrap_or_default(); + let fragment = ctx.fragment.clone().unwrap_or_default(); + return Err(ResolveError::PackagePathNotExported( + format!("./{}{query}{fragment}", subpath.trim_start_matches('.')), + package_url.join("package.json"), + )); + } + // 1. Let mainExport be undefined. + let main_export = match exports { + // 2. If exports is a String or Array, or an Object containing no keys starting with ".", then + JSONValue::String(_) | JSONValue::Array(_) => { + // 1. Set mainExport to exports. + Some(exports) + } + // 3. Otherwise if exports is an Object containing a "." property, then + JSONValue::Object(map) => { + // 1. Set mainExport to exports["."]. + map.get(".").map_or_else( + || { + if map.keys().any(|key| key.starts_with("./") || key.starts_with('#')) { + None + } else { + Some(exports) + } + }, + Some, + ) + } + _ => None, + }; + // 4. If mainExport is not undefined, then + if let Some(main_export) = main_export { + // 1. Let resolved be the result of PACKAGE_TARGET_RESOLVE( packageURL, mainExport, null, false, conditions). + let resolved = self.package_target_resolve( + package_url, + ".", + main_export, + None, + /* is_imports */ false, + conditions, + ctx, + )?; + // 2. If resolved is not null or undefined, return resolved. + if let Some(path) = resolved { + return Ok(Some(path)); + } + } + } + // 3. Otherwise, if exports is an Object and all keys of exports start with ".", then + if let JSONValue::Object(exports) = exports { + // 1. Let matchKey be the string "./" concatenated with subpath. + // Note: `package_imports_exports_resolve` does not require the leading dot. + let match_key = &subpath; + // 2. Let resolved be the result of PACKAGE_IMPORTS_EXPORTS_RESOLVE( matchKey, exports, packageURL, false, conditions). + if let Some(path) = self.package_imports_exports_resolve( + match_key, + exports, + package_url, + /* is_imports */ false, + conditions, + ctx, + )? { + // 3. If resolved is not null or undefined, return resolved. + return Ok(Some(path)); + } + } + // 4. Throw a Package Path Not Exported error. + Err(ResolveError::PackagePathNotExported( + subpath.to_string(), + package_url.join("package.json"), + )) + } + + /// PACKAGE_IMPORTS_RESOLVE(specifier, parentURL, conditions) + fn package_imports_resolve( + &self, + specifier: &str, + package_json: &PackageJson, + ctx: &mut Ctx, + ) -> Result, ResolveError> { + // 1. Assert: specifier begins with "#". + debug_assert!(specifier.starts_with('#'), "{specifier}"); + // 2. If specifier is exactly equal to "#" or starts with "#/", then + // 1. Throw an Invalid Module Specifier error. + // 3. Let packageURL be the result of LOOKUP_PACKAGE_SCOPE(parentURL). + // 4. If packageURL is not null, then + + // 1. Let pjson be the result of READ_PACKAGE_JSON(packageURL). + // 2. If pjson.imports is a non-null Object, then + + // 1. Let resolved be the result of PACKAGE_IMPORTS_EXPORTS_RESOLVE( specifier, pjson.imports, packageURL, true, conditions). + let mut has_imports = false; + for imports in package_json.imports_fields(&self.options.imports_fields) { + if !has_imports { + has_imports = true; + // TODO: fill in test case for this case + if specifier == "#" || specifier.starts_with("#/") { + return Err(ResolveError::InvalidModuleSpecifier( + specifier.to_string(), + package_json.path.clone(), + )); + } + } + if let Some(path) = self.package_imports_exports_resolve( + specifier, + imports, + package_json.directory(), + /* is_imports */ true, + &self.options.condition_names, + ctx, + )? { + // 2. If resolved is not null or undefined, return resolved. + return Ok(Some(path)); + } + } + + // 5. Throw a Package Import Not Defined error. + if has_imports { + Err(ResolveError::PackageImportNotDefined( + specifier.to_string(), + package_json.path.clone(), + )) + } else { + Ok(None) + } + } + + /// PACKAGE_IMPORTS_EXPORTS_RESOLVE(matchKey, matchObj, packageURL, isImports, conditions) + fn package_imports_exports_resolve( + &self, + match_key: &str, + match_obj: &JSONMap, + package_url: &Path, + is_imports: bool, + conditions: &[String], + ctx: &mut Ctx, + ) -> ResolveResult { + // enhanced-resolve behaves differently, it throws + // Error: CachedPath to directories is not possible with the exports field (specifier was ./dist/) + if match_key.ends_with('/') { + return Ok(None); + } + // 1. If matchKey is a key of matchObj and does not contain "*", then + if !match_key.contains('*') { + // 1. Let target be the value of matchObj[matchKey]. + if let Some(target) = match_obj.get(match_key) { + // 2. Return the result of PACKAGE_TARGET_RESOLVE(packageURL, target, null, isImports, conditions). + return self.package_target_resolve( + package_url, + match_key, + target, + None, + is_imports, + conditions, + ctx, + ); + } + } + + let mut best_target = None; + let mut best_match = ""; + let mut best_key = ""; + // 2. Let expansionKeys be the list of keys of matchObj containing only a single "*", sorted by the sorting function PATTERN_KEY_COMPARE which orders in descending order of specificity. + // 3. For each key expansionKey in expansionKeys, do + for (expansion_key, target) in match_obj { + if expansion_key.starts_with("./") || expansion_key.starts_with('#') { + // 1. Let patternBase be the substring of expansionKey up to but excluding the first "*" character. + if let Some((pattern_base, pattern_trailer)) = expansion_key.split_once('*') { + // 2. If matchKey starts with but is not equal to patternBase, then + if match_key.starts_with(pattern_base) + // 1. Let patternTrailer be the substring of expansionKey from the index after the first "*" character. + && !pattern_trailer.contains('*') + // 2. If patternTrailer has zero length, or if matchKey ends with patternTrailer and the length of matchKey is greater than or equal to the length of expansionKey, then + && (pattern_trailer.is_empty() + || (match_key.len() >= expansion_key.len() + && match_key.ends_with(pattern_trailer))) + && Self::pattern_key_compare(best_key, expansion_key).is_gt() + { + // 1. Let target be the value of matchObj[expansionKey]. + best_target = Some(target); + // 2. Let patternMatch be the substring of matchKey starting at the index of the length of patternBase up to the length of matchKey minus the length of patternTrailer. + best_match = + &match_key[pattern_base.len()..match_key.len() - pattern_trailer.len()]; + best_key = expansion_key; + } + } else if expansion_key.ends_with('/') + && match_key.starts_with(expansion_key) + && Self::pattern_key_compare(best_key, expansion_key).is_gt() + { + // TODO: [DEP0148] DeprecationWarning: Use of deprecated folder mapping "./dist/" in the "exports" field module resolution of the package at xxx/package.json. + best_target = Some(target); + best_match = &match_key[expansion_key.len()..]; + best_key = expansion_key; + } + } + } + if let Some(best_target) = best_target { + // 3. Return the result of PACKAGE_TARGET_RESOLVE(packageURL, target, patternMatch, isImports, conditions). + return self.package_target_resolve( + package_url, + best_key, + best_target, + Some(best_match), + is_imports, + conditions, + ctx, + ); + } + // 4. Return null. + Ok(None) + } + + /// PACKAGE_TARGET_RESOLVE(packageURL, target, patternMatch, isImports, conditions) + #[allow(clippy::too_many_arguments)] + fn package_target_resolve( + &self, + package_url: &Path, + target_key: &str, + target: &JSONValue, + pattern_match: Option<&str>, + is_imports: bool, + conditions: &[String], + ctx: &mut Ctx, + ) -> ResolveResult { + fn normalize_string_target<'a>( + target_key: &'a str, + target: &'a str, + pattern_match: Option<&'a str>, + package_url: &Path, + ) -> Result, ResolveError> { + let target = if let Some(pattern_match) = pattern_match { + if !target_key.contains('*') && !target.contains('*') { + // enhanced-resolve behaviour + // TODO: [DEP0148] DeprecationWarning: Use of deprecated folder mapping "./dist/" in the "exports" field module resolution of the package at xxx/package.json. + if target_key.ends_with('/') && target.ends_with('/') { + Cow::Owned(format!("{target}{pattern_match}")) + } else { + return Err(ResolveError::InvalidPackageConfigDirectory( + package_url.join("package.json"), + )); + } + } else { + Cow::Owned(target.replace('*', pattern_match)) + } + } else { + Cow::Borrowed(target) + }; + Ok(target) + } + + match target { + // 1. If target is a String, then + JSONValue::String(target) => { + // 1. If target does not start with "./", then + if !target.starts_with("./") { + // 1. If isImports is false, or if target starts with "../" or "/", or if target is a valid URL, then + if !is_imports || target.starts_with("../") || target.starts_with('/') { + // 1. Throw an Invalid Package Target error. + return Err(ResolveError::InvalidPackageTarget( + target.to_string(), + target_key.to_string(), + package_url.join("package.json"), + )); + } + // 2. If patternMatch is a String, then + // 1. Return PACKAGE_RESOLVE(target with every instance of "*" replaced by patternMatch, packageURL + "/"). + let target = + normalize_string_target(target_key, target, pattern_match, package_url)?; + let package_url = self.cache.value(package_url); + // // 3. Return PACKAGE_RESOLVE(target, packageURL + "/"). + return self.package_resolve(&package_url, &target, ctx); + } + + // 2. If target split on "/" or "\" contains any "", ".", "..", or "node_modules" segments after the first "." segment, case insensitive and including percent encoded variants, throw an Invalid Package Target error. + // 3. Let resolvedTarget be the URL resolution of the concatenation of packageURL and target. + // 4. Assert: resolvedTarget is contained in packageURL. + // 5. If patternMatch is null, then + let target = + normalize_string_target(target_key, target, pattern_match, package_url)?; + if Path::new(target.as_ref()).is_invalid_exports_target() { + return Err(ResolveError::InvalidPackageTarget( + target.to_string(), + target_key.to_string(), + package_url.join("package.json"), + )); + } + let resolved_target = package_url.normalize_with(target.as_ref()); + // 6. If patternMatch split on "/" or "\" contains any "", ".", "..", or "node_modules" segments, case insensitive and including percent encoded variants, throw an Invalid Module Specifier error. + // 7. Return the URL resolution of resolvedTarget with every instance of "*" replaced with patternMatch. + let value = self.cache.value(&resolved_target); + return Ok(Some(value)); + } + // 2. Otherwise, if target is a non-null Object, then + JSONValue::Object(target) => { + // 1. If exports contains any index property keys, as defined in ECMA-262 6.1.7 Array Index, throw an Invalid Package Configuration error. + // 2. For each property p of target, in object insertion order as, + for (key, target_value) in target { + // 1. If p equals "default" or conditions contains an entry for p, then + if key == "default" || conditions.contains(key) { + // 1. Let targetValue be the value of the p property in target. + // 2. Let resolved be the result of PACKAGE_TARGET_RESOLVE( packageURL, targetValue, patternMatch, isImports, conditions). + let resolved = self.package_target_resolve( + package_url, + target_key, + target_value, + pattern_match, + is_imports, + conditions, + ctx, + ); + // 3. If resolved is equal to undefined, continue the loop. + if let Some(path) = resolved? { + // 4. Return resolved. + return Ok(Some(path)); + } + } + } + // 3. Return undefined. + return Ok(None); + } + // 3. Otherwise, if target is an Array, then + JSONValue::Array(targets) => { + // 1. If _target.length is zero, return null. + if targets.is_empty() { + // Note: return PackagePathNotExported has the same effect as return because there are no matches. + return Err(ResolveError::PackagePathNotExported( + pattern_match.unwrap_or(".").to_string(), + package_url.join("package.json"), + )); + } + // 2. For each item targetValue in target, do + for (i, target_value) in targets.iter().enumerate() { + // 1. Let resolved be the result of PACKAGE_TARGET_RESOLVE( packageURL, targetValue, patternMatch, isImports, conditions), continuing the loop on any Invalid Package Target error. + let resolved = self.package_target_resolve( + package_url, + target_key, + target_value, + pattern_match, + is_imports, + conditions, + ctx, + ); + + if resolved.is_err() && i == targets.len() { + return resolved; + } + + // 2. If resolved is undefined, continue the loop. + if let Ok(Some(path)) = resolved { + // 3. Return resolved. + return Ok(Some(path)); + } + } + // 3. Return or throw the last fallback resolution null return or error. + // Note: see `resolved.is_err() && i == targets.len()` + } + _ => {} + } + // 4. Otherwise, if target is null, return null. + Ok(None) + // 5. Otherwise throw an Invalid Package Target error. + } + + // Returns (module, subpath) + // https://github.com/nodejs/node/blob/8f0f17e1e3b6c4e58ce748e06343c5304062c491/lib/internal/modules/esm/resolve.js#L688 + fn parse_package_specifier(specifier: &str) -> (&str, &str) { + let mut separator_index = specifier.as_bytes().iter().position(|b| *b == b'/'); + // let mut valid_package_name = true; + // let mut is_scoped = false; + if specifier.starts_with('@') { + // is_scoped = true; + if separator_index.is_none() || specifier.is_empty() { + // valid_package_name = false; + } else if let Some(index) = &separator_index { + separator_index = specifier[*index + 1..] + .as_bytes() + .iter() + .position(|b| *b == b'/') + .map(|i| i + *index + 1); + } + } + let package_name = + separator_index.map_or(specifier, |separator_index| &specifier[..separator_index]); + + // TODO: https://github.com/nodejs/node/blob/8f0f17e1e3b6c4e58ce748e06343c5304062c491/lib/internal/modules/esm/resolve.js#L705C1-L714C1 + // Package name cannot have leading . and cannot have percent-encoding or + // \\ separators. + // if (RegExpPrototypeExec(invalidPackageNameRegEx, packageName) !== null) + // validPackageName = false; + + // if (!validPackageName) { + // throw new ERR_INVALID_MODULE_SPECIFIER( + // specifier, 'is not a valid package name', fileURLToPath(base)); + // } + let package_subpath = + separator_index.map_or("", |separator_index| &specifier[separator_index..]); + (package_name, package_subpath) + } + + /// PATTERN_KEY_COMPARE(keyA, keyB) + fn pattern_key_compare(key_a: &str, key_b: &str) -> Ordering { + if key_a.is_empty() { + return Ordering::Greater; + } + // 1. Assert: keyA ends with "/" or contains only a single "*". + debug_assert!(key_a.ends_with('/') || key_a.match_indices('*').count() == 1, "{key_a}"); + // 2. Assert: keyB ends with "/" or contains only a single "*". + debug_assert!(key_b.ends_with('/') || key_b.match_indices('*').count() == 1, "{key_b}"); + // 3. Let baseLengthA be the index of "*" in keyA plus one, if keyA contains "*", or the length of keyA otherwise. + let a_pos = key_a.chars().position(|c| c == '*'); + let base_length_a = a_pos.map_or(key_a.len(), |p| p + 1); + // 4. Let baseLengthB be the index of "*" in keyB plus one, if keyB contains "*", or the length of keyB otherwise. + let b_pos = key_b.chars().position(|c| c == '*'); + let base_length_b = b_pos.map_or(key_b.len(), |p| p + 1); + // 5. If baseLengthA is greater than baseLengthB, return -1. + if base_length_a > base_length_b { + return Ordering::Less; + } + // 6. If baseLengthB is greater than baseLengthA, return 1. + if base_length_b > base_length_a { + return Ordering::Greater; + } + // 7. If keyA does not contain "*", return 1. + if !key_a.contains('*') { + return Ordering::Greater; + } + // 8. If keyB does not contain "*", return -1. + if !key_b.contains('*') { + return Ordering::Less; + } + // 9. If the length of keyA is greater than the length of keyB, return -1. + if key_a.len() > key_b.len() { + return Ordering::Less; + } + // 10. If the length of keyB is greater than the length of keyA, return 1. + if key_b.len() > key_a.len() { + return Ordering::Greater; + } + // 11. Return 0. + Ordering::Equal + } + + fn strip_package_name<'a>(specifier: &'a str, package_name: &'a str) -> Option<&'a str> { + specifier + .strip_prefix(package_name) + .filter(|tail| tail.is_empty() || tail.starts_with(SLASH_START)) + } +} diff --git a/Source/options.rs b/Source/options.rs new file mode 100644 index 00000000..7893890e --- /dev/null +++ b/Source/options.rs @@ -0,0 +1,633 @@ +use std::path::Path; +use std::{fmt, path::PathBuf}; + +/// Module Resolution Options +/// +/// Options are directly ported from [enhanced-resolve](https://github.com/webpack/enhanced-resolve#resolver-options). +/// +/// See [webpack resolve](https://webpack.js.org/configuration/resolve/) for information and examples +#[derive(Debug, Clone)] +pub struct ResolveOptions { + /// Path to TypeScript configuration file. + /// + /// Default `None` + pub tsconfig: Option, + + /// Create aliases to import or require certain modules more easily. + /// + /// An alias is used to replace a whole path or part of a path. + /// For example, to alias a commonly used `src/` folders: `vec![("@/src"), vec![AliasValue::Path("/path/to/src")]]` + /// + /// A trailing $ can also be added to the given object's keys to signify an exact match. + /// + /// See [webpack's `resolve.alias` documentation](https://webpack.js.org/configuration/resolve/#resolvealias) for a list of use cases. + pub alias: Alias, + + /// A list of alias fields in description files. + /// + /// Specify a field, such as `browser`, to be parsed according to [this specification](https://github.com/defunctzombie/package-browser-field-spec). + /// Can be a path to json object such as `["path", "to", "exports"]`. + /// + /// Default `[]` + pub alias_fields: Vec>, + + /// Condition names for exports field which defines entry points of a package. + /// + /// The key order in the exports field is significant. During condition matching, earlier entries have higher priority and take precedence over later entries. + /// + /// Default `[]` + pub condition_names: Vec, + + /// The JSON files to use for descriptions. (There was once a `bower.json`.) + /// + /// Default `["package.json"]` + pub description_files: Vec, + + /// Set to [EnforceExtension::Enabled] for [ESM Mandatory file extensions](https://nodejs.org/api/esm.html#mandatory-file-extensions). + /// + /// If `enforce_extension` is set to [EnforceExtension::Enabled], resolution will not allow extension-less files. + /// This means `require('./foo.js')` will resolve, while `require('./foo')` will not. + /// + /// The default value for `enforce_extension` is [EnforceExtension::Auto], which is changed upon initialization. + /// + /// It changes to [EnforceExtension::Enabled] if [ResolveOptions::extensions] contains an empty string; + /// otherwise, this value changes to [EnforceExtension::Disabled]. + /// + /// Explicitly set the value to [EnforceExtension::Disabled] to disable this automatic behavior. + /// + /// For reference, this behavior is aligned with `enhanced-resolve`. See . + pub enforce_extension: EnforceExtension, + + /// A list of exports fields in description files. + /// + /// Can be a path to a JSON object such as `["path", "to", "exports"]`. + /// + /// Default `[["exports"]]`. + pub exports_fields: Vec>, + + /// Fields from `package.json` which are used to provide the internal requests of a package + /// (requests starting with # are considered internal). + /// + /// Can be a path to a JSON object such as `["path", "to", "imports"]`. + /// + /// Default `[["imports"]]`. + pub imports_fields: Vec>, + + /// An object which maps extension to extension aliases. + /// + /// Default `{}` + pub extension_alias: Vec<(String, Vec)>, + + /// Attempt to resolve these extensions in order. + /// + /// If multiple files share the same name but have different extensions, + /// will resolve the one with the extension listed first in the array and skip the rest. + /// + /// All extensions must have a leading dot. + /// + /// Default `[".js", ".json", ".node"]` + pub extensions: Vec, + + /// Redirect module requests when normal resolving fails. + /// + /// Default `[]` + pub fallback: Alias, + + /// Request passed to resolve is already fully specified and extensions or main files are not resolved for it (they are still resolved for internal requests). + /// + /// See also webpack configuration [resolve.fullySpecified](https://webpack.js.org/configuration/module/#resolvefullyspecified) + /// + /// Default `false` + pub fully_specified: bool, + + /// A list of main fields in description files + /// + /// Default `["main"]`. + pub main_fields: Vec, + + /// The filename to be used while resolving directories. + /// + /// Default `["index"]` + pub main_files: Vec, + + /// A list of directories to resolve modules from, can be absolute path or folder name. + /// + /// Default `["node_modules"]` + pub modules: Vec, + + /// A manifest loaded from pnp::load_pnp_manifest. + /// + /// Default `None` + #[cfg(feature = "yarn_pnp")] + pub pnp_manifest: Option, + + /// Resolve to a context instead of a file. + /// + /// Default `false` + pub resolve_to_context: bool, + + /// Prefer to resolve module requests as relative requests instead of using modules from node_modules directories. + /// + /// Default `false` + pub prefer_relative: bool, + + /// Prefer to resolve server-relative urls as absolute paths before falling back to resolve in ResolveOptions::roots. + /// + /// Default `false` + pub prefer_absolute: bool, + + /// A list of resolve restrictions to restrict the paths that a request can be resolved on. + /// + /// Default `[]` + pub restrictions: Vec, + + /// A list of directories where requests of server-relative URLs (starting with '/') are resolved. + /// On non-Windows systems these requests are resolved as an absolute path first. + /// + /// Default `[]` + pub roots: Vec, + + /// Whether to resolve symlinks to their symlinked location. + /// When enabled, symlinked resources are resolved to their real path, not their symlinked location. + /// Note that this may cause module resolution to fail when using tools that symlink packages (like npm link). + /// + /// Default `true` + pub symlinks: bool, + + /// Whether to parse [module.builtinModules](https://nodejs.org/api/module.html#modulebuiltinmodules) or not. + /// For example, "zlib" will throw [crate::ResolveError::Builtin] when set to true. + /// + /// Default `false` + pub builtin_modules: bool, +} + +impl ResolveOptions { + /// ## Examples + /// + /// ``` + /// use oxc_resolver::ResolveOptions; + /// + /// let options = ResolveOptions::default().with_condition_names(&["bar"]); + /// assert_eq!(options.condition_names, vec!["bar".to_string()]) + /// ``` + #[must_use] + pub fn with_condition_names(mut self, names: &[&str]) -> Self { + self.condition_names = names.iter().map(ToString::to_string).collect::>(); + self + } + + /// ## Examples + /// + /// ``` + /// use oxc_resolver::ResolveOptions; + /// + /// let options = ResolveOptions::default().with_builtin_modules(false); + /// assert_eq!(options.builtin_modules, false) + /// ``` + #[must_use] + pub fn with_builtin_modules(mut self, flag: bool) -> Self { + self.builtin_modules = flag; + self + } + + /// Adds a single root to the options + /// + /// ## Examples + /// + /// ``` + /// use oxc_resolver::ResolveOptions; + /// use std::path::{Path, PathBuf}; + /// + /// let options = ResolveOptions::default().with_root("foo"); + /// assert_eq!(options.roots, vec![PathBuf::from("foo")]) + /// ``` + #[must_use] + pub fn with_root>(mut self, root: P) -> Self { + self.roots.push(root.as_ref().to_path_buf()); + self + } + + /// Adds a single extension to the list of extensions + /// + /// ## Examples + /// + /// ``` + /// use oxc_resolver::ResolveOptions; + /// use std::path::{Path, PathBuf}; + /// + /// let options = ResolveOptions::default().with_extension("jsonc"); + /// assert!(options.extensions.contains(&"jsonc".to_string())); + /// ``` + #[must_use] + pub fn with_extension>(mut self, extension: S) -> Self { + self.extensions.push(extension.into()); + self + } + + /// Adds a single main field to the list of fields + /// + /// ## Examples + /// + /// ``` + /// use oxc_resolver::ResolveOptions; + /// use std::path::{Path, PathBuf}; + /// + /// let options = ResolveOptions::default().with_main_field("something"); + /// assert!(options.main_fields.contains(&"something".to_string())); + /// ``` + #[must_use] + pub fn with_main_field>(mut self, field: S) -> Self { + self.main_fields.push(field.into()); + self + } + + /// Changes how the extension should be treated + /// + /// ## Examples + /// + /// ``` + /// use oxc_resolver::{ResolveOptions, EnforceExtension}; + /// use std::path::{Path, PathBuf}; + /// + /// let options = ResolveOptions::default().with_force_extension(EnforceExtension::Enabled); + /// assert_eq!(options.enforce_extension, EnforceExtension::Enabled); + /// ``` + #[must_use] + pub fn with_force_extension(mut self, enforce_extension: EnforceExtension) -> Self { + self.enforce_extension = enforce_extension; + self + } + + /// Sets the value for [ResolveOptions::fully_specified] + /// + /// ## Examples + /// + /// ``` + /// use oxc_resolver::{ResolveOptions}; + /// use std::path::{Path, PathBuf}; + /// + /// let options = ResolveOptions::default().with_fully_specified(true); + /// assert_eq!(options.fully_specified, true); + /// ``` + #[must_use] + pub fn with_fully_specified(mut self, fully_specified: bool) -> Self { + self.fully_specified = fully_specified; + self + } + /// Sets the value for [ResolveOptions::prefer_relative] + /// + /// ## Examples + /// + /// ``` + /// use oxc_resolver::{ResolveOptions}; + /// use std::path::{Path, PathBuf}; + /// + /// let options = ResolveOptions::default().with_prefer_relative(true); + /// assert_eq!(options.prefer_relative, true); + /// ``` + #[must_use] + pub fn with_prefer_relative(mut self, flag: bool) -> Self { + self.prefer_relative = flag; + self + } + /// Sets the value for [ResolveOptions::prefer_absolute] + /// + /// ## Examples + /// + /// ``` + /// use oxc_resolver::{ResolveOptions}; + /// use std::path::{Path, PathBuf}; + /// + /// let options = ResolveOptions::default().with_prefer_absolute(true); + /// assert_eq!(options.prefer_absolute, true); + /// ``` + #[must_use] + pub fn with_prefer_absolute(mut self, flag: bool) -> Self { + self.prefer_absolute = flag; + self + } + + /// Changes the value of [ResolveOptions::symlinks] + /// + /// ## Examples + /// + /// ``` + /// use oxc_resolver::{ResolveOptions}; + /// + /// let options = ResolveOptions::default().with_symbolic_link(false); + /// assert_eq!(options.symlinks, false); + /// ``` + #[must_use] + pub fn with_symbolic_link(mut self, flag: bool) -> Self { + self.symlinks = flag; + self + } + + /// Adds a module to [ResolveOptions::modules] + /// + /// ## Examples + /// + /// ``` + /// use oxc_resolver::{ResolveOptions}; + /// + /// let options = ResolveOptions::default().with_module("module"); + /// assert!(options.modules.contains(&"module".to_string())); + /// ``` + #[must_use] + pub fn with_module>(mut self, module: M) -> Self { + self.modules.push(module.into()); + self + } + + /// Adds a main file to [ResolveOptions::main_files] + /// + /// ## Examples + /// + /// ``` + /// use oxc_resolver::{ResolveOptions}; + /// + /// let options = ResolveOptions::default().with_main_file("foo"); + /// assert!(options.main_files.contains(&"foo".to_string())); + /// ``` + #[must_use] + pub fn with_main_file>(mut self, module: M) -> Self { + self.main_files.push(module.into()); + self + } + + pub(crate) fn sanitize(mut self) -> Self { + debug_assert!( + self.extensions.iter().filter(|e| !e.is_empty()).all(|e| e.starts_with('.')), + "All extensions must start with a leading dot" + ); + // Set `enforceExtension` to `true` when [ResolveOptions::extensions] contains an empty string. + // See + if self.enforce_extension == EnforceExtension::Auto { + if !self.extensions.is_empty() && self.extensions.iter().any(String::is_empty) { + self.enforce_extension = EnforceExtension::Enabled; + } else { + self.enforce_extension = EnforceExtension::Disabled; + } + } + self + } +} + +/// Value for [ResolveOptions::enforce_extension] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum EnforceExtension { + Auto, + Enabled, + Disabled, +} + +impl EnforceExtension { + pub const fn is_auto(&self) -> bool { + matches!(self, Self::Auto) + } + + pub const fn is_enabled(&self) -> bool { + matches!(self, Self::Enabled) + } + + pub const fn is_disabled(&self) -> bool { + matches!(self, Self::Disabled) + } +} + +/// Alias for [ResolveOptions::alias] and [ResolveOptions::fallback] +pub type Alias = Vec<(String, Vec)>; + +/// Alias Value for [ResolveOptions::alias] and [ResolveOptions::fallback] +#[derive(Debug, Clone, Hash, PartialEq, Eq)] +pub enum AliasValue { + /// The path value + Path(String), + + /// The `false` value + Ignore, +} + +impl From for AliasValue +where + S: Into, +{ + fn from(value: S) -> Self { + Self::Path(value.into()) + } +} + +/// Value for [ResolveOptions::restrictions] +#[derive(Debug, Clone)] +pub enum Restriction { + Path(PathBuf), + RegExp(String), +} + +/// Tsconfig Options for [ResolveOptions::tsconfig] +/// +/// Derived from [tsconfig-paths-webpack-plugin](https://github.com/dividab/tsconfig-paths-webpack-plugin#options) +#[derive(Debug, Clone)] +pub struct TsconfigOptions { + /// Allows you to specify where to find the TypeScript configuration file. + /// You may provide + /// * a relative path to the configuration file. It will be resolved relative to cwd. + /// * an absolute path to the configuration file. + pub config_file: PathBuf, + + /// Support for Typescript Project References. + pub references: TsconfigReferences, +} + +/// Configuration for [TsconfigOptions::references] +#[derive(Debug, Clone)] +pub enum TsconfigReferences { + Disabled, + /// Use the `references` field from tsconfig of `config_file`. + Auto, + /// Manually provided relative or absolute path. + Paths(Vec), +} + +impl Default for ResolveOptions { + fn default() -> Self { + Self { + tsconfig: None, + alias: vec![], + alias_fields: vec![], + condition_names: vec![], + description_files: vec!["package.json".into()], + enforce_extension: EnforceExtension::Auto, + extension_alias: vec![], + exports_fields: vec![vec!["exports".into()]], + imports_fields: vec![vec!["imports".into()]], + extensions: vec![".js".into(), ".json".into(), ".node".into()], + fallback: vec![], + fully_specified: false, + main_fields: vec!["main".into()], + main_files: vec!["index".into()], + modules: vec!["node_modules".into()], + #[cfg(feature = "yarn_pnp")] + pnp_manifest: None, + resolve_to_context: false, + prefer_relative: false, + prefer_absolute: false, + restrictions: vec![], + roots: vec![], + symlinks: true, + builtin_modules: false, + } + } +} + +// For tracing +impl fmt::Display for ResolveOptions { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + if let Some(tsconfig) = &self.tsconfig { + write!(f, "tsconfig:{tsconfig:?},")?; + } + if !self.alias.is_empty() { + write!(f, "alias:{:?},", self.alias)?; + } + if !self.alias_fields.is_empty() { + write!(f, "alias_fields:{:?},", self.alias_fields)?; + } + if !self.condition_names.is_empty() { + write!(f, "condition_names:{:?},", self.condition_names)?; + } + if self.enforce_extension.is_enabled() { + write!(f, "enforce_extension:{:?},", self.enforce_extension)?; + } + if !self.exports_fields.is_empty() { + write!(f, "exports_fields:{:?},", self.exports_fields)?; + } + if !self.imports_fields.is_empty() { + write!(f, "imports_fields:{:?},", self.imports_fields)?; + } + if !self.extension_alias.is_empty() { + write!(f, "extension_alias:{:?},", self.extension_alias)?; + } + if !self.extensions.is_empty() { + write!(f, "extensions:{:?},", self.extensions)?; + } + if !self.fallback.is_empty() { + write!(f, "fallback:{:?},", self.fallback)?; + } + if self.fully_specified { + write!(f, "fully_specified:{:?},", self.fully_specified)?; + } + if !self.main_fields.is_empty() { + write!(f, "main_fields:{:?},", self.main_fields)?; + } + if !self.main_files.is_empty() { + write!(f, "main_files:{:?},", self.main_files)?; + } + if !self.modules.is_empty() { + write!(f, "modules:{:?},", self.modules)?; + } + if self.resolve_to_context { + write!(f, "resolve_to_context:{:?},", self.resolve_to_context)?; + } + if self.prefer_relative { + write!(f, "prefer_relative:{:?},", self.prefer_relative)?; + } + if self.prefer_absolute { + write!(f, "prefer_absolute:{:?},", self.prefer_absolute)?; + } + if !self.restrictions.is_empty() { + write!(f, "restrictions:{:?},", self.restrictions)?; + } + if !self.roots.is_empty() { + write!(f, "roots:{:?},", self.roots)?; + } + if self.symlinks { + write!(f, "symlinks:{:?},", self.symlinks)?; + } + if self.builtin_modules { + write!(f, "builtin_modules:{:?},", self.builtin_modules)?; + } + Ok(()) + } +} + +#[cfg(test)] +mod test { + use super::{ + AliasValue, EnforceExtension, ResolveOptions, Restriction, TsconfigOptions, + TsconfigReferences, + }; + use std::path::PathBuf; + + #[test] + fn enforce_extension() { + assert!(EnforceExtension::Auto.is_auto()); + assert!(!EnforceExtension::Enabled.is_auto()); + assert!(!EnforceExtension::Disabled.is_auto()); + + assert!(!EnforceExtension::Auto.is_enabled()); + assert!(EnforceExtension::Enabled.is_enabled()); + assert!(!EnforceExtension::Disabled.is_enabled()); + + assert!(!EnforceExtension::Auto.is_disabled()); + assert!(!EnforceExtension::Enabled.is_disabled()); + assert!(EnforceExtension::Disabled.is_disabled()); + } + + #[test] + fn display() { + let options = ResolveOptions { + tsconfig: Some(TsconfigOptions { + config_file: PathBuf::from("tsconfig.json"), + references: TsconfigReferences::Auto, + }), + alias: vec![("a".into(), vec![AliasValue::Ignore])], + alias_fields: vec![vec!["browser".into()]], + condition_names: vec!["require".into()], + enforce_extension: EnforceExtension::Enabled, + extension_alias: vec![(".js".into(), vec![".ts".into()])], + exports_fields: vec![vec!["exports".into()]], + imports_fields: vec![vec!["imports".into()]], + fallback: vec![("fallback".into(), vec![AliasValue::Ignore])], + fully_specified: true, + resolve_to_context: true, + prefer_relative: true, + prefer_absolute: true, + restrictions: vec![Restriction::Path(PathBuf::from("restrictions"))], + roots: vec![PathBuf::from("roots")], + builtin_modules: true, + ..ResolveOptions::default() + }; + + let expected = r#"tsconfig:TsconfigOptions { config_file: "tsconfig.json", references: Auto },alias:[("a", [Ignore])],alias_fields:[["browser"]],condition_names:["require"],enforce_extension:Enabled,exports_fields:[["exports"]],imports_fields:[["imports"]],extension_alias:[(".js", [".ts"])],extensions:[".js", ".json", ".node"],fallback:[("fallback", [Ignore])],fully_specified:true,main_fields:["main"],main_files:["index"],modules:["node_modules"],resolve_to_context:true,prefer_relative:true,prefer_absolute:true,restrictions:[Path("restrictions")],roots:["roots"],symlinks:true,builtin_modules:true,"#; + assert_eq!(format!("{options}"), expected); + + let options = ResolveOptions { + alias: vec![], + alias_fields: vec![], + builtin_modules: false, + condition_names: vec![], + description_files: vec![], + enforce_extension: EnforceExtension::Disabled, + exports_fields: vec![], + extension_alias: vec![], + extensions: vec![], + fallback: vec![], + fully_specified: false, + imports_fields: vec![], + main_fields: vec![], + main_files: vec![], + modules: vec![], + #[cfg(feature = "yarn_pnp")] + pnp_manifest: None, + prefer_absolute: false, + prefer_relative: false, + resolve_to_context: false, + restrictions: vec![], + roots: vec![], + symlinks: false, + tsconfig: None, + }; + + assert_eq!(format!("{options}"), ""); + } +} diff --git a/Source/package_json.rs b/Source/package_json.rs new file mode 100644 index 00000000..7e05d202 --- /dev/null +++ b/Source/package_json.rs @@ -0,0 +1,219 @@ +//! package.json definitions +//! +//! Code related to export field are copied from [Parcel's resolver](https://github.com/parcel-bundler/parcel/blob/v2/packages/utils/node-resolver-rs/src/package_json.rs) +use std::path::{Path, PathBuf}; + +use serde_json::Value as JSONValue; + +use crate::{path::PathUtil, ResolveError}; + +pub type JSONMap = serde_json::Map; + +/// Deserialized package.json +#[derive(Debug, Default)] +pub struct PackageJson { + /// Path to `package.json`. Contains the `package.json` filename. + pub path: PathBuf, + + /// Realpath to `package.json`. Contains the `package.json` filename. + pub realpath: PathBuf, + + /// The "name" field defines your package's name. + /// The "name" field can be used in addition to the "exports" field to self-reference a package using its name. + /// + /// + pub name: Option, + + /// The "type" field. + /// + /// + pub r#type: Option, + + /// The "sideEffects" field. + /// + /// + pub side_effects: Option, + + raw_json: std::sync::Arc, +} + +impl PackageJson { + /// # Panics + /// # Errors + pub(crate) fn parse( + path: PathBuf, + realpath: PathBuf, + json: &str, + ) -> Result { + let mut raw_json: JSONValue = serde_json::from_str(json)?; + let mut package_json = Self::default(); + + if let Some(json_object) = raw_json.as_object_mut() { + // Remove large fields that are useless for pragmatic use. + #[cfg(feature = "package_json_raw_json_api")] + { + json_object.remove("description"); + json_object.remove("keywords"); + json_object.remove("scripts"); + json_object.remove("dependencies"); + json_object.remove("devDependencies"); + json_object.remove("peerDependencies"); + json_object.remove("optionalDependencies"); + } + + // Add name, type and sideEffects. + package_json.name = + json_object.get("name").and_then(|field| field.as_str()).map(ToString::to_string); + package_json.r#type = json_object.get("type").cloned(); + package_json.side_effects = json_object.get("sideEffects").cloned(); + } + + package_json.path = path; + package_json.realpath = realpath; + package_json.raw_json = std::sync::Arc::new(raw_json); + Ok(package_json) + } + + fn get_value_by_path<'a>( + fields: &'a serde_json::Map, + path: &[String], + ) -> Option<&'a JSONValue> { + if path.is_empty() { + return None; + } + let mut value = fields.get(&path[0])?; + for key in path.iter().skip(1) { + if let Some(inner_value) = value.as_object().and_then(|o| o.get(key)) { + value = inner_value; + } else { + return None; + } + } + Some(value) + } + + /// Raw serde json value of `package.json`. + /// + /// This is currently used in Rspack for: + /// * getting the `sideEffects` field + /// * query in - search on GitHub indicates query on the `type` field. + /// + /// To reduce overall memory consumption, large fields that useless for pragmatic use are removed. + /// They are: `description`, `keywords`, `scripts`, + /// `dependencies` and `devDependencies`, `peerDependencies`, `optionalDependencies`. + #[cfg(feature = "package_json_raw_json_api")] + pub fn raw_json(&self) -> &std::sync::Arc { + &self.raw_json + } + + /// Directory to `package.json` + /// + /// # Panics + /// + /// * When the package.json path is misconfigured. + pub fn directory(&self) -> &Path { + debug_assert!(self.realpath.file_name().is_some_and(|x| x == "package.json")); + self.realpath.parent().unwrap() + } + + /// The "main" field defines the entry point of a package when imported by name via a node_modules lookup. Its value is a path. + /// + /// When a package has an "exports" field, this will take precedence over the "main" field when importing the package by name. + /// + /// Values are dynamically retrieved from [ResolveOptions::main_fields]. + /// + /// + pub(crate) fn main_fields<'a>( + &'a self, + main_fields: &'a [String], + ) -> impl Iterator + '_ { + main_fields + .iter() + .filter_map(|main_field| self.raw_json.get(main_field)) + .filter_map(|value| value.as_str()) + } + + /// The "exports" field allows defining the entry points of a package when imported by name loaded either via a node_modules lookup or a self-reference to its own name. + /// + /// + pub(crate) fn exports_fields<'a>( + &'a self, + exports_fields: &'a [Vec], + ) -> impl Iterator + '_ { + exports_fields.iter().filter_map(|object_path| { + self.raw_json + .as_object() + .and_then(|json_object| Self::get_value_by_path(json_object, object_path)) + }) + } + + /// In addition to the "exports" field, there is a package "imports" field to create private mappings that only apply to import specifiers from within the package itself. + /// + /// + pub(crate) fn imports_fields<'a>( + &'a self, + imports_fields: &'a [Vec], + ) -> impl Iterator + '_ { + imports_fields.iter().filter_map(|object_path| { + self.raw_json + .as_object() + .and_then(|json_object| Self::get_value_by_path(json_object, object_path)) + .and_then(|value| value.as_object()) + }) + } + + /// The "browser" field is provided by a module author as a hint to javascript bundlers or component tools when packaging modules for client side use. + /// Multiple values are configured by [ResolveOptions::alias_fields]. + /// + /// + fn browser_fields<'a>( + &'a self, + alias_fields: &'a [Vec], + ) -> impl Iterator + '_ { + alias_fields.iter().filter_map(|object_path| { + self.raw_json + .as_object() + .and_then(|json_object| Self::get_value_by_path(json_object, object_path)) + // Only object is valid, all other types are invalid + // https://github.com/webpack/enhanced-resolve/blob/3a28f47788de794d9da4d1702a3a583d8422cd48/lib/AliasFieldPlugin.js#L44-L52 + .and_then(|value| value.as_object()) + }) + } + + /// Resolve the request string for this package.json by looking at the `browser` field. + /// + /// # Errors + /// + /// * Returns [ResolveError::Ignored] for `"path": false` in `browser` field. + pub(crate) fn resolve_browser_field<'a>( + &'a self, + path: &Path, + request: Option<&str>, + alias_fields: &'a [Vec], + ) -> Result, ResolveError> { + for object in self.browser_fields(alias_fields) { + if let Some(request) = request { + if let Some(value) = object.get(request) { + return Self::alias_value(path, value); + } + } else { + let dir = self.path.parent().unwrap(); + for (key, value) in object { + let joined = dir.normalize_with(key); + if joined == path { + return Self::alias_value(path, value); + } + } + } + } + Ok(None) + } + + fn alias_value<'a>(key: &Path, value: &'a JSONValue) -> Result, ResolveError> { + match value { + JSONValue::String(value) => Ok(Some(value.as_str())), + JSONValue::Bool(b) if !b => Err(ResolveError::Ignored(key.to_path_buf())), + _ => Ok(None), + } + } +} diff --git a/Source/path.rs b/Source/path.rs new file mode 100644 index 00000000..f66fdd76 --- /dev/null +++ b/Source/path.rs @@ -0,0 +1,132 @@ +//! Path Utilities +//! +//! Code adapted from the following libraries +//! * [path-absolutize](https://docs.rs/path-absolutize) +//! * [normalize_path](https://docs.rs/normalize-path) +use std::path::{Component, Path, PathBuf}; + +pub const SLASH_START: &[char; 2] = &['/', '\\']; + +/// Extension trait to add path normalization to std's [`Path`]. +pub trait PathUtil { + /// Normalize this path without performing I/O. + /// + /// All redundant separator and up-level references are collapsed. + /// + /// However, this does not resolve links. + fn normalize(&self) -> PathBuf; + + /// Normalize with subpath assuming this path is normalized without performing I/O. + /// + /// All redundant separator and up-level references are collapsed. + /// + /// However, this does not resolve links. + fn normalize_with>(&self, subpath: P) -> PathBuf; + + /// Defined in ESM PACKAGE_TARGET_RESOLVE + /// If target split on "/" or "\" contains any "", ".", "..", or "node_modules" segments after the first "." segment, case insensitive and including percent encoded variants + fn is_invalid_exports_target(&self) -> bool; +} + +impl PathUtil for Path { + // https://github.com/parcel-bundler/parcel/blob/e0b99c2a42e9109a9ecbd6f537844a1b33e7faf5/packages/utils/node-resolver-rs/src/path.rs#L7 + fn normalize(&self) -> PathBuf { + let mut components = self.components().peekable(); + let mut ret = if let Some(c @ Component::Prefix(..)) = components.peek() { + let buf = PathBuf::from(c.as_os_str()); + components.next(); + buf + } else { + PathBuf::new() + }; + + for component in components { + match component { + Component::Prefix(..) => unreachable!("Path {:?}", self), + Component::RootDir => { + ret.push(component.as_os_str()); + } + Component::CurDir => {} + Component::ParentDir => { + ret.pop(); + } + Component::Normal(c) => { + ret.push(c); + } + } + } + + ret + } + + // https://github.com/parcel-bundler/parcel/blob/e0b99c2a42e9109a9ecbd6f537844a1b33e7faf5/packages/utils/node-resolver-rs/src/path.rs#L37 + fn normalize_with>(&self, subpath: B) -> PathBuf { + let subpath = subpath.as_ref(); + + let mut components = subpath.components(); + + let Some(head) = components.next() else { return subpath.to_path_buf() }; + + if matches!(head, Component::Prefix(..) | Component::RootDir) { + return subpath.to_path_buf(); + } + + let mut ret = self.to_path_buf(); + for component in std::iter::once(head).chain(components) { + match component { + Component::CurDir => {} + Component::ParentDir => { + ret.pop(); + } + Component::Normal(c) => { + ret.push(c); + } + Component::Prefix(..) | Component::RootDir => { + unreachable!("Path {:?} Subpath {:?}", self, subpath) + } + } + } + + ret + } + + fn is_invalid_exports_target(&self) -> bool { + self.components().enumerate().any(|(index, c)| match c { + Component::ParentDir => true, + Component::CurDir => index > 0, + Component::Normal(c) => c.eq_ignore_ascii_case("node_modules"), + _ => false, + }) + } +} + +// https://github.com/webpack/enhanced-resolve/blob/main/test/path.test.js +#[test] +fn is_invalid_exports_target() { + let test_cases = [ + "../a.js", + "../", + "./a/b/../../../c.js", + "./a/b/../../../", + "./../../c.js", + "./../../", + "./a/../b/../../c.js", + "./a/../b/../../", + "./././../", + ]; + + for case in test_cases { + assert!(Path::new(case).is_invalid_exports_target(), "{case}"); + } + + assert!(!Path::new("C:").is_invalid_exports_target()); + assert!(!Path::new("/").is_invalid_exports_target()); +} + +#[test] +fn normalize() { + assert_eq!(Path::new("/foo/.././foo/").normalize(), Path::new("/foo")); + assert_eq!(Path::new("C://").normalize(), Path::new("C://")); + assert_eq!(Path::new("C:").normalize(), Path::new("C:")); + assert_eq!(Path::new(r"\\server\share").normalize(), Path::new(r"\\server\share")); +} diff --git a/Source/resolution.rs b/Source/resolution.rs new file mode 100644 index 00000000..a3aaa2e7 --- /dev/null +++ b/Source/resolution.rs @@ -0,0 +1,92 @@ +use crate::package_json::PackageJson; +use std::{ + fmt, + path::{Path, PathBuf}, + sync::Arc, +}; + +/// The final path resolution with optional `?query` and `#fragment` +#[derive(Clone)] +pub struct Resolution { + pub(crate) path: PathBuf, + + /// path query `?query`, contains `?`. + pub(crate) query: Option, + + /// path fragment `#query`, contains `#`. + pub(crate) fragment: Option, + + pub(crate) package_json: Option>, +} + +impl fmt::Debug for Resolution { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("Resolution") + .field("path", &self.path) + .field("query", &self.query) + .field("fragment", &self.fragment) + .field("package_json", &self.package_json.as_ref().map(|p| &p.path)) + .finish() + } +} + +impl PartialEq for Resolution { + fn eq(&self, other: &Self) -> bool { + self.path == other.path && self.query == other.query && self.fragment == other.fragment + } +} +impl Eq for Resolution {} + +impl Resolution { + /// Returns the path without query and fragment + pub fn path(&self) -> &Path { + &self.path + } + + /// Returns the path without query and fragment + pub fn into_path_buf(self) -> PathBuf { + self.path + } + + /// Returns the path query `?query`, contains the leading `?` + pub fn query(&self) -> Option<&str> { + self.query.as_deref() + } + + /// Returns the path fragment `#fragment`, contains the leading `#` + pub fn fragment(&self) -> Option<&str> { + self.fragment.as_deref() + } + + /// Returns serialized package_json + pub fn package_json(&self) -> Option<&Arc> { + self.package_json.as_ref() + } + + /// Returns the full path with query and fragment + pub fn full_path(&self) -> PathBuf { + let mut path = self.path.clone().into_os_string(); + if let Some(query) = &self.query { + path.push(query); + } + if let Some(fragment) = &self.fragment { + path.push(fragment); + } + PathBuf::from(path) + } +} + +#[test] +fn test() { + let resolution = Resolution { + path: PathBuf::from("foo"), + query: Some("?query".to_string()), + fragment: Some("#fragment".to_string()), + package_json: None, + }; + assert_eq!(resolution.path(), Path::new("foo")); + assert_eq!(resolution.query(), Some("?query")); + assert_eq!(resolution.fragment(), Some("#fragment")); + assert_eq!(resolution.full_path(), PathBuf::from("foo?query#fragment")); + assert_eq!(resolution.into_path_buf(), PathBuf::from("foo")); +} diff --git a/Source/specifier.rs b/Source/specifier.rs new file mode 100644 index 00000000..73ed038c --- /dev/null +++ b/Source/specifier.rs @@ -0,0 +1,230 @@ +use crate::error::SpecifierError; +use std::borrow::Cow; + +#[derive(Debug)] +pub struct Specifier<'a> { + path: Cow<'a, str>, + pub query: Option<&'a str>, + pub fragment: Option<&'a str>, +} + +impl<'a> Specifier<'a> { + pub fn path(&'a self) -> &'a str { + self.path.as_ref() + } + + pub fn parse(specifier: &'a str) -> Result { + if specifier.is_empty() { + return Err(SpecifierError::Empty(specifier.to_string())); + } + let offset = match specifier.as_bytes()[0] { + b'/' | b'.' | b'#' => 1, + _ => 0, + }; + let (path, query, fragment) = Self::parse_query_framgment(specifier, offset); + if path.is_empty() { + return Err(SpecifierError::Empty(specifier.to_string())); + } + Ok(Self { path, query, fragment }) + } + + fn parse_query_framgment( + specifier: &'a str, + skip: usize, + ) -> (Cow<'a, str>, Option<&str>, Option<&str>) { + let mut query_start: Option = None; + let mut fragment_start: Option = None; + + let mut prev = specifier.chars().next().unwrap(); + let mut escaped_indexes = vec![]; + for (i, c) in specifier.char_indices().skip(skip) { + if c == '?' && query_start.is_none() { + query_start = Some(i); + } + if c == '#' { + if prev == '\0' { + escaped_indexes.push(i - 1); + } else { + fragment_start = Some(i); + break; + } + } + prev = c; + } + + let (path, query, fragment) = match (query_start, fragment_start) { + (Some(i), Some(j)) => { + debug_assert!(i < j); + (&specifier[..i], Some(&specifier[i..j]), Some(&specifier[j..])) + } + (Some(i), None) => (&specifier[..i], Some(&specifier[i..]), None), + (None, Some(j)) => (&specifier[..j], None, Some(&specifier[j..])), + _ => (specifier, None, None), + }; + + let path = if escaped_indexes.is_empty() { + Cow::Borrowed(path) + } else { + // Remove the `\0` characters for a legal path. + Cow::Owned( + path.chars() + .enumerate() + .filter_map(|(i, c)| (!escaped_indexes.contains(&i)).then_some(c)) + .collect::(), + ) + }; + + (path, query, fragment) + } +} + +#[cfg(test)] +mod tests { + use super::{Specifier, SpecifierError}; + + #[test] + fn debug() { + let specifier = Specifier::parse("/").unwrap(); + assert_eq!( + format!("{specifier:?}"), + r#"Specifier { path: "/", query: None, fragment: None }"# + ); + } + + #[test] + fn empty() { + let specifiers = ["", "?"]; + for specifier in specifiers { + let error = Specifier::parse(specifier).unwrap_err(); + assert_eq!(error, SpecifierError::Empty(specifier.to_string())); + } + } + + #[test] + fn absolute() -> Result<(), SpecifierError> { + let specifier = "/test?#"; + let parsed = Specifier::parse(specifier)?; + assert_eq!(parsed.path, "/test"); + assert_eq!(parsed.query, Some("?")); + assert_eq!(parsed.fragment, Some("#")); + Ok(()) + } + + #[test] + fn relative() -> Result<(), SpecifierError> { + let specifiers = ["./test", "../test", "../../test"]; + for specifier in specifiers { + let mut r = specifier.to_string(); + r.push_str("?#"); + let parsed = Specifier::parse(&r)?; + assert_eq!(parsed.path, specifier); + assert_eq!(parsed.query, Some("?")); + assert_eq!(parsed.fragment, Some("#")); + } + Ok(()) + } + + #[test] + fn hash() -> Result<(), SpecifierError> { + let specifiers = ["#", "#path"]; + for specifier in specifiers { + let mut r = specifier.to_string(); + r.push_str("?#"); + let parsed = Specifier::parse(&r)?; + assert_eq!(parsed.path, specifier); + assert_eq!(parsed.query, Some("?")); + assert_eq!(parsed.fragment, Some("#")); + } + Ok(()) + } + + #[test] + fn module() -> Result<(), SpecifierError> { + let specifiers = ["module"]; + for specifier in specifiers { + let mut r = specifier.to_string(); + r.push_str("?#"); + let parsed = Specifier::parse(&r)?; + assert_eq!(parsed.path, specifier); + assert_eq!(parsed.query, Some("?")); + assert_eq!(parsed.fragment, Some("#")); + } + Ok(()) + } + + #[test] + fn query_fragment() -> Result<(), SpecifierError> { + let data = [ + ("a?", Some("?"), None), + ("a?query", Some("?query"), None), + ("a?query1?query2", Some("?query1?query2"), None), + ("a?query1?query2?query3", Some("?query1?query2?query3"), None), + ("a#", None, Some("#")), + ("a#b#c", None, Some("#b#c")), + ("a#fragment", None, Some("#fragment")), + ("a?#", Some("?"), Some("#")), + ("a?#fragment", Some("?"), Some("#fragment")), + ("a?query#", Some("?query"), Some("#")), + ("a?query#fragment", Some("?query"), Some("#fragment")), + ("a#fragment?", None, Some("#fragment?")), + ("a#fragment?query", None, Some("#fragment?query")), + ]; + + for (specifier_str, query, fragment) in data { + let specifier = Specifier::parse(specifier_str)?; + assert_eq!(specifier.path, "a", "{specifier_str}"); + assert_eq!(specifier.query, query, "{specifier_str}"); + assert_eq!(specifier.fragment, fragment, "{specifier_str}"); + } + + Ok(()) + } + + #[test] + // https://github.com/webpack/enhanced-resolve/blob/main/test/identifier.test.js + fn enhanced_resolve_edge_cases() -> Result<(), SpecifierError> { + let data = [ + ("path/#", "path/", "", "#"), + ("path/as/?", "path/as/", "?", ""), + ("path/#/?", "path/", "", "#/?"), + ("path/#repo#hash", "path/", "", "#repo#hash"), + ("path/#r#hash", "path/", "", "#r#hash"), + ("path/#repo/#repo2#hash", "path/", "", "#repo/#repo2#hash"), + ("path/#r/#r#hash", "path/", "", "#r/#r#hash"), + ("path/#/not/a/hash?not-a-query", "path/", "", "#/not/a/hash?not-a-query"), + ]; + + for (specifier_str, path, query, fragment) in data { + let specifier = Specifier::parse(specifier_str)?; + assert_eq!(specifier.path, path, "{specifier_str}"); + assert_eq!(specifier.query.unwrap_or(""), query, "{specifier_str}"); + assert_eq!(specifier.fragment.unwrap_or(""), fragment, "{specifier_str}"); + } + + Ok(()) + } + + // https://github.com/webpack/enhanced-resolve/blob/main/test/identifier.test.js + #[test] + fn enhanced_resolve_windows_like() -> Result<(), SpecifierError> { + let data = [ + ("path\\#", "path\\", "", "#"), + ("path\\as\\?", "path\\as\\", "?", ""), + ("path\\#\\?", "path\\", "", "#\\?"), + ("path\\#repo#hash", "path\\", "", "#repo#hash"), + ("path\\#r#hash", "path\\", "", "#r#hash"), + ("path\\#repo\\#repo2#hash", "path\\", "", "#repo\\#repo2#hash"), + ("path\\#r\\#r#hash", "path\\", "", "#r\\#r#hash"), + ("path\\#/not/a/hash?not-a-query", "path\\", "", "#/not/a/hash?not-a-query"), + ]; + + for (specifier_str, path, query, fragment) in data { + let specifier = Specifier::parse(specifier_str)?; + assert_eq!(specifier.path, path, "{specifier_str}"); + assert_eq!(specifier.query.unwrap_or(""), query, "{specifier_str}"); + assert_eq!(specifier.fragment.unwrap_or(""), fragment, "{specifier_str}"); + } + + Ok(()) + } +} diff --git a/Source/tsconfig.rs b/Source/tsconfig.rs new file mode 100644 index 00000000..6a675442 --- /dev/null +++ b/Source/tsconfig.rs @@ -0,0 +1,223 @@ +use std::{ + hash::BuildHasherDefault, + path::{Path, PathBuf}, + sync::Arc, +}; + +use indexmap::IndexMap; +use rustc_hash::FxHasher; +use serde::Deserialize; + +use crate::PathUtil; + +pub type CompilerOptionsPathsMap = IndexMap, BuildHasherDefault>; + +#[derive(Debug, Clone, Eq, PartialEq, Deserialize)] +#[serde(untagged)] +pub enum ExtendsField { + Single(String), + Multiple(Vec), +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct TsConfig { + /// Whether this is the caller tsconfig. + /// Used for final template variable substitution when all configs are extended and merged. + #[serde(skip)] + root: bool, + + /// Path to `tsconfig.json`. Contains the `tsconfig.json` filename. + #[serde(skip)] + pub(crate) path: PathBuf, + + #[serde(default)] + pub extends: Option, + + #[serde(default)] + pub compiler_options: CompilerOptions, + + /// Bubbled up project references with a reference to their tsconfig. + #[serde(default)] + pub references: Vec, +} + +/// Compiler Options +/// +/// +#[derive(Debug, Default, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CompilerOptions { + base_url: Option, + + /// Path aliases + paths: Option, + + /// The actual base for where path aliases are resolved from. + #[serde(skip)] + paths_base: PathBuf, +} + +/// Project Reference +/// +/// +#[derive(Debug, Deserialize)] +pub struct ProjectReference { + /// The path property of each reference can point to a directory containing a tsconfig.json file, + /// or to the config file itself (which may have any name). + pub path: PathBuf, + + /// Reference to the resolved tsconfig + #[serde(skip)] + pub tsconfig: Option>, +} + +impl TsConfig { + pub fn parse(root: bool, path: &Path, json: &mut str) -> Result { + _ = json_strip_comments::strip(json); + let mut tsconfig: Self = serde_json::from_str(json)?; + tsconfig.root = root; + tsconfig.path = path.to_path_buf(); + let directory = tsconfig.directory().to_path_buf(); + if let Some(base_url) = tsconfig.compiler_options.base_url { + tsconfig.compiler_options.base_url = Some(directory.normalize_with(base_url)); + } + if tsconfig.compiler_options.paths.is_some() { + tsconfig.compiler_options.paths_base = + tsconfig.compiler_options.base_url.as_ref().map_or(directory, Clone::clone); + } + Ok(tsconfig) + } + + pub fn build(mut self) -> Self { + if self.root { + let dir = self.directory().to_path_buf(); + // Substitute template variable in `tsconfig.compilerOptions.paths` + if let Some(paths) = &mut self.compiler_options.paths { + for paths in paths.values_mut() { + for path in paths { + Self::substitute_template_variable(&dir, path); + } + } + } + } + self + } + + /// Directory to `tsconfig.json` + /// + /// # Panics + /// + /// * When the `tsconfig.json` path is misconfigured. + pub fn directory(&self) -> &Path { + debug_assert!(self.path.file_name().is_some()); + self.path.parent().unwrap() + } + + pub fn extend_tsconfig(&mut self, tsconfig: &Self) { + let compiler_options = &mut self.compiler_options; + if compiler_options.paths.is_none() { + compiler_options.paths_base = compiler_options + .base_url + .as_ref() + .map_or_else(|| tsconfig.compiler_options.paths_base.clone(), Clone::clone); + compiler_options.paths.clone_from(&tsconfig.compiler_options.paths); + } + if compiler_options.base_url.is_none() { + compiler_options.base_url.clone_from(&tsconfig.compiler_options.base_url); + } + } + + pub fn resolve(&self, path: &Path, specifier: &str) -> Vec { + if path.starts_with(self.base_path()) { + let paths = self.resolve_path_alias(specifier); + if !paths.is_empty() { + return paths; + } + } + for tsconfig in self.references.iter().filter_map(|reference| reference.tsconfig.as_ref()) { + if path.starts_with(tsconfig.base_path()) { + return tsconfig.resolve_path_alias(specifier); + } + } + vec![] + } + + // Copied from parcel + // + pub fn resolve_path_alias(&self, specifier: &str) -> Vec { + if specifier.starts_with(['/', '.']) { + return vec![]; + } + + let base_url_iter = self + .compiler_options + .base_url + .as_ref() + .map_or_else(Vec::new, |base_url| vec![base_url.normalize_with(specifier)]); + + let Some(paths_map) = &self.compiler_options.paths else { + return base_url_iter; + }; + + let paths = paths_map.get(specifier).map_or_else( + || { + let mut longest_prefix_length = 0; + let mut longest_suffix_length = 0; + let mut best_key: Option<&String> = None; + + for key in paths_map.keys() { + if let Some((prefix, suffix)) = key.split_once('*') { + if (best_key.is_none() || prefix.len() > longest_prefix_length) + && specifier.starts_with(prefix) + && specifier.ends_with(suffix) + { + longest_prefix_length = prefix.len(); + longest_suffix_length = suffix.len(); + best_key.replace(key); + } + } + } + + best_key.and_then(|key| paths_map.get(key)).map_or_else(Vec::new, |paths| { + paths + .iter() + .map(|path| { + path.replace( + '*', + &specifier[longest_prefix_length + ..specifier.len() - longest_suffix_length], + ) + }) + .collect::>() + }) + }, + Clone::clone, + ); + + paths + .into_iter() + .map(|p| self.compiler_options.paths_base.normalize_with(p)) + .chain(base_url_iter) + .collect() + } + + fn base_path(&self) -> &Path { + self.compiler_options + .base_url + .as_ref() + .map_or_else(|| self.directory(), |path| path.as_ref()) + } + + /// Template variable `${configDir}` for substitution of config files directory path + /// + /// NOTE: All tests cases are just a head replacement of `${configDir}`, so we are constrained as such. + /// + /// See + fn substitute_template_variable(directory: &Path, path: &mut String) { + const TEMPLATE_VARIABLE: &str = "${configDir}/"; + if let Some(stripped_path) = path.strip_prefix(TEMPLATE_VARIABLE) { + *path = directory.join(stripped_path).to_string_lossy().to_string(); + } + } +} diff --git a/fixtures/pnpm/package.json b/fixtures/pnpm/package.json index b763d74d..d2bb83b5 100644 --- a/fixtures/pnpm/package.json +++ b/fixtures/pnpm/package.json @@ -3,7 +3,7 @@ "version": "1.0.0", "private": true, "devDependencies": { - "axios": "1.6.2", + "axios": "1.7.4", "ipaddr.js": "2.2.0", "postcss": "8.4.33", "styled-components": "6.1.1" diff --git a/napi/Source/lib.rs b/napi/Source/lib.rs new file mode 100644 index 00000000..7cd839c5 --- /dev/null +++ b/napi/Source/lib.rs @@ -0,0 +1,214 @@ +extern crate napi; +extern crate napi_derive; +extern crate oxc_resolver; + +use std::{ + path::{Path, PathBuf}, + sync::Arc, +}; + +use napi::{bindgen_prelude::AsyncTask, Task}; +use napi_derive::napi; +use oxc_resolver::{ResolveOptions, Resolver}; + +use self::{ + options::{NapiResolveOptions, StrOrStrList}, + tracing::init_tracing, +}; + +mod options; +mod tracing; + +#[napi(object)] +pub struct ResolveResult { + pub path: Option, + pub error: Option, + /// "type" field in the package.json file + pub module_type: Option, +} + +fn resolve(resolver: &Resolver, path: &Path, request: &str) -> ResolveResult { + match resolver.resolve(path, request) { + Ok(resolution) => ResolveResult { + path: Some(resolution.full_path().to_string_lossy().to_string()), + error: None, + module_type: resolution + .package_json() + .and_then(|p| p.r#type.as_ref()) + .and_then(|t| t.as_str()) + .map(|t| t.to_string()), + }, + Err(err) => ResolveResult { path: None, module_type: None, error: Some(err.to_string()) }, + } +} + +#[allow(clippy::needless_pass_by_value)] +#[napi] +pub fn sync(path: String, request: String) -> ResolveResult { + let path = PathBuf::from(path); + let resolver = Resolver::new(ResolveOptions::default()); + resolve(&resolver, &path, &request) +} + +pub struct ResolveTask { + resolver: Arc, + directory: PathBuf, + request: String, +} + +#[napi] +impl Task for ResolveTask { + type Output = ResolveResult; + type JsValue = ResolveResult; + + fn compute(&mut self) -> napi::Result { + Ok(resolve(&self.resolver, &self.directory, &self.request)) + } + + fn resolve(&mut self, _: napi::Env, result: Self::Output) -> napi::Result { + Ok(result) + } +} + +#[napi] +pub struct ResolverFactory { + resolver: Arc, +} + +#[napi] +impl ResolverFactory { + #[napi(constructor)] + pub fn new(options: NapiResolveOptions) -> Self { + init_tracing(); + Self { resolver: Arc::new(Resolver::new(Self::normalize_options(options))) } + } + + #[napi] + pub fn default() -> Self { + let default_options = ResolveOptions::default(); + Self { resolver: Arc::new(Resolver::new(default_options)) } + } + + /// Clone the resolver using the same underlying cache. + #[napi] + pub fn clone_with_options(&self, options: NapiResolveOptions) -> Self { + Self { + resolver: Arc::new(self.resolver.clone_with_options(Self::normalize_options(options))), + } + } + + /// Clear the underlying cache. + #[napi] + pub fn clear_cache(&self) { + self.resolver.clear_cache(); + } + + /// Synchronously resolve `specifier` at an absolute path to a `directory`. + #[allow(clippy::needless_pass_by_value)] + #[napi] + pub fn sync(&self, directory: String, request: String) -> ResolveResult { + let path = PathBuf::from(directory); + resolve(&self.resolver, &path, &request) + } + + /// Asynchronously resolve `specifier` at an absolute path to a `directory`. + #[allow(clippy::needless_pass_by_value)] + #[napi(js_name = "async")] + pub fn resolve_async(&self, directory: String, request: String) -> AsyncTask { + let path = PathBuf::from(directory); + let resolver = self.resolver.clone(); + AsyncTask::new(ResolveTask { resolver, directory: path, request }) + } + + fn normalize_options(op: NapiResolveOptions) -> ResolveOptions { + let default = ResolveOptions::default(); + // merging options + ResolveOptions { + tsconfig: op.tsconfig.map(|tsconfig| tsconfig.into()), + alias: op + .alias + .map(|alias| { + alias + .into_iter() + .map(|(k, v)| { + let v = v + .into_iter() + .map(|item| match item { + Some(path) => oxc_resolver::AliasValue::from(path), + None => oxc_resolver::AliasValue::Ignore, + }) + .collect(); + (k, v) + }) + .collect::>() + }) + .unwrap_or(default.alias), + alias_fields: op + .alias_fields + .map(|o| o.into_iter().map(|x| StrOrStrList(x).into()).collect::>()) + .unwrap_or(default.alias_fields), + condition_names: op.condition_names.unwrap_or(default.condition_names), + description_files: op.description_files.unwrap_or(default.description_files), + enforce_extension: op + .enforce_extension + .map(|enforce_extension| enforce_extension.into()) + .unwrap_or(default.enforce_extension), + exports_fields: op + .exports_fields + .map(|o| o.into_iter().map(|x| StrOrStrList(x).into()).collect::>()) + .unwrap_or(default.exports_fields), + imports_fields: op + .imports_fields + .map(|o| o.into_iter().map(|x| StrOrStrList(x).into()).collect::>()) + .unwrap_or(default.imports_fields), + extension_alias: op + .extension_alias + .map(|extension_alias| extension_alias.into_iter().collect::>()) + .unwrap_or(default.extension_alias), + extensions: op.extensions.unwrap_or(default.extensions), + fallback: op + .fallback + .map(|fallback| { + fallback + .into_iter() + .map(|(k, v)| { + let v = v + .into_iter() + .map(|item| match item { + Some(path) => oxc_resolver::AliasValue::from(path), + None => oxc_resolver::AliasValue::Ignore, + }) + .collect(); + (k, v) + }) + .collect::>() + }) + .unwrap_or(default.fallback), + fully_specified: op.fully_specified.unwrap_or(default.fully_specified), + main_fields: op + .main_fields + .map(|o| StrOrStrList(o).into()) + .unwrap_or(default.main_fields), + main_files: op.main_files.unwrap_or(default.main_files), + modules: op.modules.map(|o| StrOrStrList(o).into()).unwrap_or(default.modules), + resolve_to_context: op.resolve_to_context.unwrap_or(default.resolve_to_context), + prefer_relative: op.prefer_relative.unwrap_or(default.prefer_relative), + prefer_absolute: op.prefer_absolute.unwrap_or(default.prefer_absolute), + restrictions: op + .restrictions + .map(|restrictions| { + restrictions + .into_iter() + .map(|restriction| restriction.into()) + .collect::>() + }) + .unwrap_or(default.restrictions), + roots: op + .roots + .map(|roots| roots.into_iter().map(PathBuf::from).collect::>()) + .unwrap_or(default.roots), + symlinks: op.symlinks.unwrap_or(default.symlinks), + builtin_modules: op.builtin_modules.unwrap_or(default.builtin_modules), + } + } +} diff --git a/napi/Source/options.rs b/napi/Source/options.rs new file mode 100644 index 00000000..ea35672f --- /dev/null +++ b/napi/Source/options.rs @@ -0,0 +1,261 @@ +use std::path::PathBuf; + +use napi::Either; +use napi_derive::napi; +use std::collections::HashMap; + +/// Module Resolution Options +/// +/// Options are directly ported from [enhanced-resolve](https://github.com/webpack/enhanced-resolve#resolver-options). +/// +/// See [webpack resolve](https://webpack.js.org/configuration/resolve/) for information and examples +#[derive(Debug, Clone)] +#[napi(object)] +pub struct NapiResolveOptions { + /// Path to TypeScript configuration file. + /// + /// Default `None` + pub tsconfig: Option, + + /// Alias for [ResolveOptions::alias] and [ResolveOptions::fallback]. + /// + /// For the second value of the tuple, `None -> AliasValue::Ignore`, Some(String) -> + /// AliasValue::Path(String)` + /// Create aliases to import or require certain modules more easily. + /// A trailing $ can also be added to the given object's keys to signify an exact match. + pub alias: Option>>>, + + /// A list of alias fields in description files. + /// Specify a field, such as `browser`, to be parsed according to [this specification](https://github.com/defunctzombie/package-browser-field-spec). + /// Can be a path to json object such as `["path", "to", "exports"]`. + /// + /// Default `[]` + #[napi(ts_type = "(string | string[])[]")] + pub alias_fields: Option>, + + /// Condition names for exports field which defines entry points of a package. + /// The key order in the exports field is significant. During condition matching, earlier entries have higher priority and take precedence over later entries. + /// + /// Default `[]` + pub condition_names: Option>, + + /// The JSON files to use for descriptions. (There was once a `bower.json`.) + /// + /// Default `["package.json"]` + pub description_files: Option>, + + /// If true, it will not allow extension-less files. + /// So by default `require('./foo')` works if `./foo` has a `.js` extension, + /// but with this enabled only `require('./foo.js')` will work. + /// + /// Default to `true` when [ResolveOptions::extensions] contains an empty string. + /// Use `Some(false)` to disable the behavior. + /// See + /// + /// Default None, which is the same as `Some(false)` when the above empty rule is not applied. + pub enforce_extension: Option, + + /// A list of exports fields in description files. + /// Can be a path to json object such as `["path", "to", "exports"]`. + /// + /// Default `[["exports"]]`. + #[napi(ts_type = "(string | string[])[]")] + pub exports_fields: Option>, + + /// Fields from `package.json` which are used to provide the internal requests of a package + /// (requests starting with # are considered internal). + /// + /// Can be a path to a JSON object such as `["path", "to", "imports"]`. + /// + /// Default `[["imports"]]`. + #[napi(ts_type = "(string | string[])[]")] + pub imports_fields: Option>, + + /// An object which maps extension to extension aliases. + /// + /// Default `{}` + pub extension_alias: Option>>, + + /// Attempt to resolve these extensions in order. + /// If multiple files share the same name but have different extensions, + /// will resolve the one with the extension listed first in the array and skip the rest. + /// + /// Default `[".js", ".json", ".node"]` + pub extensions: Option>, + + /// Redirect module requests when normal resolving fails. + /// + /// Default `[]` + pub fallback: Option>>>, + + /// Request passed to resolve is already fully specified and extensions or main files are not resolved for it (they are still resolved for internal requests). + /// + /// See also webpack configuration [resolve.fullySpecified](https://webpack.js.org/configuration/module/#resolvefullyspecified) + /// + /// Default `false` + pub fully_specified: Option, + + /// A list of main fields in description files + /// + /// Default `["main"]`. + #[napi(ts_type = "string | string[]")] + pub main_fields: Option, + + /// The filename to be used while resolving directories. + /// + /// Default `["index"]` + pub main_files: Option>, + + /// A list of directories to resolve modules from, can be absolute path or folder name. + /// + /// Default `["node_modules"]` + #[napi(ts_type = "string | string[]")] + pub modules: Option, + + /// Resolve to a context instead of a file. + /// + /// Default `false` + pub resolve_to_context: Option, + + /// Prefer to resolve module requests as relative requests instead of using modules from node_modules directories. + /// + /// Default `false` + pub prefer_relative: Option, + + /// Prefer to resolve server-relative urls as absolute paths before falling back to resolve in ResolveOptions::roots. + /// + /// Default `false` + pub prefer_absolute: Option, + + /// A list of resolve restrictions to restrict the paths that a request can be resolved on. + /// + /// Default `[]` + pub restrictions: Option>, + + /// A list of directories where requests of server-relative URLs (starting with '/') are resolved. + /// On non-Windows systems these requests are resolved as an absolute path first. + /// + /// Default `[]` + pub roots: Option>, + + /// Whether to resolve symlinks to their symlinked location. + /// When enabled, symlinked resources are resolved to their real path, not their symlinked location. + /// Note that this may cause module resolution to fail when using tools that symlink packages (like npm link). + /// + /// Default `true` + pub symlinks: Option, + + /// Whether to parse [module.builtinModules](https://nodejs.org/api/module.html#modulebuiltinmodules) or not. + /// For example, "zlib" will throw [crate::ResolveError::Builtin] when set to true. + /// + /// Default `false` + pub builtin_modules: Option, +} + +#[napi] +#[derive(Debug, PartialEq, Eq)] +pub enum EnforceExtension { + Auto, + Enabled, + Disabled, +} + +impl EnforceExtension { + pub fn is_auto(&self) -> bool { + *self == Self::Auto + } + + pub fn is_enabled(&self) -> bool { + *self == Self::Enabled + } + + pub fn is_disabled(&self) -> bool { + *self == Self::Disabled + } +} + +/// Alias Value for [ResolveOptions::alias] and [ResolveOptions::fallback]. +/// Use struct because napi don't support structured union now +#[napi(object)] +#[derive(Debug, Clone)] +pub struct Restriction { + pub path: Option, + pub regex: Option, +} + +/// Tsconfig Options +/// +/// Derived from [tsconfig-paths-webpack-plugin](https://github.com/dividab/tsconfig-paths-webpack-plugin#options) +#[napi(object)] +#[derive(Debug, Clone)] +pub struct TsconfigOptions { + /// Allows you to specify where to find the TypeScript configuration file. + /// You may provide + /// * a relative path to the configuration file. It will be resolved relative to cwd. + /// * an absolute path to the configuration file. + pub config_file: String, + + /// Support for Typescript Project References. + /// + /// * `'auto'`: use the `references` field from tsconfig of `config_file`. + /// * `string[]`: manually provided relative or absolute path. + #[napi(ts_type = "'auto' | string[]")] + pub references: Option>>, +} + +impl Into for Restriction { + fn into(self) -> oxc_resolver::Restriction { + match (self.path, self.regex) { + (None, None) => { + panic!("Should specify path or regex") + } + (None, Some(regex)) => oxc_resolver::Restriction::RegExp(regex), + (Some(path), None) => oxc_resolver::Restriction::Path(PathBuf::from(path)), + (Some(_), Some(_)) => { + panic!("Restriction can't be path and regex at the same time") + } + } + } +} + +impl Into for EnforceExtension { + fn into(self) -> oxc_resolver::EnforceExtension { + match self { + EnforceExtension::Auto => oxc_resolver::EnforceExtension::Auto, + EnforceExtension::Enabled => oxc_resolver::EnforceExtension::Enabled, + EnforceExtension::Disabled => oxc_resolver::EnforceExtension::Disabled, + } + } +} + +impl Into for TsconfigOptions { + fn into(self) -> oxc_resolver::TsconfigOptions { + oxc_resolver::TsconfigOptions { + config_file: PathBuf::from(self.config_file), + references: match self.references { + Some(Either::A(string)) if string.as_str() == "auto" => { + oxc_resolver::TsconfigReferences::Auto + } + Some(Either::A(opt)) => { + panic!("`{}` is not a valid option for tsconfig references", opt) + } + Some(Either::B(paths)) => oxc_resolver::TsconfigReferences::Paths( + paths.into_iter().map(PathBuf::from).collect::>(), + ), + None => oxc_resolver::TsconfigReferences::Disabled, + }, + } + } +} + +type StrOrStrListType = Either>; +pub struct StrOrStrList(pub StrOrStrListType); + +impl Into> for StrOrStrList { + fn into(self) -> Vec { + match self { + StrOrStrList(Either::A(s)) => Vec::from([s]), + StrOrStrList(Either::B(a)) => a, + } + } +} diff --git a/napi/Source/tracing.rs b/napi/Source/tracing.rs new file mode 100644 index 00000000..88e4cd7e --- /dev/null +++ b/napi/Source/tracing.rs @@ -0,0 +1,25 @@ +use std::sync::OnceLock; + +use tracing_subscriber::filter::Targets; +use tracing_subscriber::prelude::__tracing_subscriber_SubscriberExt; +use tracing_subscriber::util::SubscriberInitExt; + +/// To debug `oxc_resolver`: +/// `OXC_LOG=DEBUG your program` +pub fn init_tracing() { + static TRACING: OnceLock<()> = OnceLock::new(); + TRACING.get_or_init(|| { + // Usage without the `regex` feature. + // + tracing_subscriber::registry() + .with(std::env::var("OXC_LOG").map_or_else( + |_| Targets::new(), + |env_var| { + use std::str::FromStr; + Targets::from_str(&env_var).unwrap() + }, + )) + .with(tracing_subscriber::fmt::layer()) + .init(); + }); +}