import * as qs from 'querystring' ; import type { VueLoaderOptions } from './' ; import type { RuleSetRule , Compiler , Plugin } from 'webpack' ; const id = 'vue-loader-plugin' ; const NS = 'vue-loader' ; const NormalModule = require ( 'webpack/lib/NormalModule' ) ; const BasicEffectRulePlugin = require ( 'webpack/lib/rules/BasicEffectRulePlugin' ) ; const BasicMatcherRulePlugin = require ( 'webpack/lib/rules/BasicMatcherRulePlugin' ) ; const UseEffectRulePlugin = require ( 'webpack/lib/rules/UseEffectRulePlugin' ) ; const RuleSetCompiler = require ( 'webpack/lib/rules/RuleSetCompiler' ) as RuleSetCompiler ; let objectMatcherRulePlugins = [ ] ; try { const ObjectMatcherRulePlugin = require ( 'webpack/lib/rules/ObjectMatcherRulePlugin' ) ; objectMatcherRulePlugins . push ( new ObjectMatcherRulePlugin ( 'assert' , 'assertions' ) , new ObjectMatcherRulePlugin ( 'descriptionData' ) ) ; } catch ( e ) { const DescriptionDataMatcherRulePlugin = require ( 'webpack/lib/rules/DescriptionDataMatcherRulePlugin' ) ; objectMatcherRulePlugins . push ( new DescriptionDataMatcherRulePlugin ( ) ) ; } type RawRule = RuleSetRule ; type RuleSetCompiler = { new ( plugins ) : { compile ( rawRules ) : RuleSet ; compileRule ( path , rawRule , refs ) : CompiledRule ; } ; } ; interface RuleSet { references : Map < string , any > ; exec ( data ) : Effect [ ] ; } interface CompiledRule { conditions : RuleCondition [ ] ; effects : Effect [ ] ; rules : CompiledRule [ ] ; oneOf : CompiledRule [ ] ; } interface RuleCondition { property : string ; matchWhenEmpty : boolean ; fn ( input ) : boolean ; } interface BasicEffect { type : 'type' | 'sideEffects' | 'parser' | 'resolve' ; value : any ; } interface UseEffect { type : 'use' ; value : { loader : string ; options : any ; ident : string ; } ; } type Effect = BasicEffect | UseEffect ; const ruleSetCompiler = new RuleSetCompiler ( [ new BasicMatcherRulePlugin ( 'test' , 'resource' ) , new BasicMatcherRulePlugin ( 'mimetype' ) , new BasicMatcherRulePlugin ( 'dependency' ) , new BasicMatcherRulePlugin ( 'include' , 'resource' ) , new BasicMatcherRulePlugin ( 'exclude' , 'resource' , true ) , new BasicMatcherRulePlugin ( 'conditions' ) , new BasicMatcherRulePlugin ( 'resource' ) , new BasicMatcherRulePlugin ( 'resourceQuery' ) , new BasicMatcherRulePlugin ( 'resourceFragment' ) , new BasicMatcherRulePlugin ( 'realResource' ) , new BasicMatcherRulePlugin ( 'issuer' ) , new BasicMatcherRulePlugin ( 'compiler' ) , ... objectMatcherRulePlugins , new BasicEffectRulePlugin ( 'type' ) , new BasicEffectRulePlugin ( 'sideEffects' ) , new BasicEffectRulePlugin ( 'parser' ) , new BasicEffectRulePlugin ( 'resolve' ) , new BasicEffectRulePlugin ( 'generator' ) , new UseEffectRulePlugin ( ) , ] ) ; class VueLoaderPlugin implements Plugin { static NS = NS ; apply ( compiler ) { const normalModule = compiler . webpack . NormalModule || NormalModule ; compiler . hooks . compilation . tap ( id , ( compilation ) => { normalModule . getCompilationHooks ( compilation ) . loader . tap ( id , ( loaderContext ) => { loaderContext [ NS ] = true ; } ) ; } ) ; const rules = compiler . options . module ! . rules ; let rawVueRule ; let vueRules = [ ] ; for ( const rawRule of rules ) { if ( rawRule . enforce ) { continue ; } vueRules = match ( rawRule , 'foo.vue' ) ; if ( ! vueRules . length ) { vueRules = match ( rawRule , 'foo.vue.html' ) ; } if ( vueRules . length > 0 ) { if ( rawRule . oneOf ) { throw new Error ( `[VueLoaderPlugin Error] vue-loader currently does not support vue rules with oneOf.` ) ; } rawVueRule = rawRule ; break ; } } if ( ! vueRules . length ) { throw new Error ( `[VueLoaderPlugin Error] No matching rule for .vue files found.\n` + `Make sure there is at least one root-level rule that matches .vue or .vue.html files.` ) ; } const vueUse = vueRules . filter ( ( rule ) => rule . type === 'use' ) . map ( ( rule ) => rule . value ) ; const vueLoaderUseIndex = vueUse . findIndex ( ( u ) => { return /^vue-loader|(\/|\\|@)vue-loader/ . test ( u . loader ) ; } ) ; if ( vueLoaderUseIndex < 0 ) { throw new Error ( `[VueLoaderPlugin Error] No matching use for vue-loader is found.\n` + `Make sure the rule matching .vue files include vue-loader in its use.` ) ; } const vueLoaderUse = vueUse [ vueLoaderUseIndex ] ; const vueLoaderOptions = ( vueLoaderUse . options = vueLoaderUse . options || { } ) as VueLoaderOptions ; const refs = new Map ( ) ; const clonedRules = rules . filter ( ( r ) => r !== rawVueRule ) . map ( ( rawRule ) => cloneRule ( rawRule , refs , langBlockRuleCheck , langBlockRuleResource ) ) ; delete rawVueRule ! . loader ; delete rawVueRule ! . options ; rawVueRule ! . use = vueUse ; const templateCompilerRule = { loader : require . resolve ( './templateLoader' ) , resourceQuery : ( query ? ) => { if ( ! query ) { return false ; } const parsed = qs . parse ( query . slice ( 1 ) ) ; return parsed . vue != null && parsed . type === 'template' ; } , options : vueLoaderOptions , } ; const jsRulesForRenderFn = rules . filter ( ( r ) => r !== rawVueRule && ( match ( r , 'test.js' ) . length > 0 || match ( r , 'test.ts' ) . length > 0 ) ) . map ( ( rawRule ) => cloneRule ( rawRule , refs , jsRuleCheck , jsRuleResource ) ) ; const pitcher = { loader : require . resolve ( './pitcher' ) , resourceQuery : ( query ? ) => { if ( ! query ) { return false ; } const parsed = qs . parse ( query . slice ( 1 ) ) ; return parsed . vue != null ; } , } ; compiler . options . module ! . rules = [ pitcher , ... jsRulesForRenderFn , templateCompilerRule , ... clonedRules , ... rules , ] ; } } const matcherCache = new WeakMap < RawRule , RuleSet > ( ) ; function match ( rule , fakeFile ) { let ruleSet = matcherCache . get ( rule ) ; if ( ! ruleSet ) { const clonedRawRule = { ... rule } ; delete clonedRawRule . include ; ruleSet = ruleSetCompiler . compile ( [ clonedRawRule ] ) ; matcherCache . set ( rule , ruleSet ) ; } return ruleSet . exec ( { resource : fakeFile , } ) ; } const langBlockRuleCheck = ( query , rule ) : boolean => { return ( query . type === 'custom' || ! rule . conditions . length || query . lang != null ) ; } ; const langBlockRuleResource = ( query , resource ) : string => `${ resource }.${ query . lang }` ; const jsRuleCheck = ( query ) : boolean => { return query . type === 'template' ; } ; const jsRuleResource = ( query , resource ) : string => `${ resource }.${ query . ts ? `ts` : `js` }` ; let uid = 0 ; function cloneRule ( rawRule , refs , ruleCheck , ruleResource ) { const compiledRule = ruleSetCompiler . compileRule ( `clonedRuleSet-${ ++ uid }` , rawRule , refs ) ; if ( ! rawRule . enforce ) { const ruleUse = compiledRule . effects . filter ( ( effect ) => effect . type === 'use' ) . map ( ( effect ) => effect . value ) ; delete rawRule . loader ; delete rawRule . options ; rawRule . use = ruleUse ; } let currentResource ; const res = { ... rawRule , resource : ( resources ) => { currentResource = resources ; return true ; } , resourceQuery : ( query ? ) => { if ( ! query ) { return false ; } const parsed = qs . parse ( query . slice ( 1 ) ) ; if ( parsed . vue == null ) { return false ; } if ( ! ruleCheck ( parsed , compiledRule ) ) { return false ; } const fakeResourcePath = ruleResource ( parsed , currentResource ) ; for ( const condition of compiledRule . conditions ) { const request = condition . property === 'resourceQuery' ? query : fakeResourcePath ; if ( condition && ! condition . fn ( request ) ) { return false ; } } return true ; } , } ; delete res . test ; if ( rawRule . rules ) { res . rules = rawRule . rules . map ( ( rule ) => cloneRule ( rule , refs , ruleCheck , ruleResource ) ) ; } if ( rawRule . oneOf ) { res . oneOf = rawRule . oneOf . map ( ( rule ) => cloneRule ( rule , refs , ruleCheck , ruleResource ) ) ; } return res ; } export default VueLoaderPlugin ;