Skip to content

Commit

Permalink
initial work
Browse files Browse the repository at this point in the history
  • Loading branch information
yyx990803 committed Mar 19, 2018
1 parent 8badc35 commit 3175633
Show file tree
Hide file tree
Showing 81 changed files with 3,875 additions and 162 deletions.
4 changes: 4 additions & 0 deletions .eslintrc.js
@@ -0,0 +1,4 @@
module.exports = {
root: true,
extends: ['plugin:vue-libs/recommended']
}
2 changes: 1 addition & 1 deletion .gitignore
@@ -1,4 +1,4 @@
.DS_Store
test
exploration
node_modules
*.log
21 changes: 21 additions & 0 deletions LICENSE
@@ -0,0 +1,21 @@
The MIT License (MIT)

Copyright (c) 2015-present Yuxi (Evan) You

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.
22 changes: 0 additions & 22 deletions lib/assemble.js

This file was deleted.

161 changes: 126 additions & 35 deletions lib/index.js
@@ -1,55 +1,146 @@
const path = require('path')
const hash = require('hash-sum')
const qs = require('querystring')
const parse = require('./parse')
const assemble = require('./assemble')
const qs = require('querystring')
const loaderUtils = require('loader-utils')
const compileTemplate = require('./template-compiler')
const selectBlock = require('./selector')
const plugin = require('./plugin')
const componentNormalizerPath = require.resolve('./runtime/componentNormalizer')

module.exports = function (source) {
const { resourcePath, resourceQuery, target, minimize, sourceMap } = this
const loaderContext = this
const stringifyRequest = r => loaderUtils.stringifyRequest(loaderContext, r)

const {
target,
request,
minimize,
sourceMap,
rootContext,
resourcePath,
resourceQuery
} = loaderContext

const isServer = target === 'node'
const isProduction = minimize || process.env.NODE_ENV === 'production'
const options = loaderUtils.getOptions(this) || {}
const options = loaderUtils.getOptions(loaderContext) || {}
const fileName = path.basename(resourcePath)
const context = this.rootContext || process.cwd()
const context = rootContext || process.cwd()
const sourceRoot = path.dirname(path.relative(context, resourcePath))
const shortFilePath = path.relative(context, resourcePath).replace(/^(\.\.[\\\/])+/, '')
const moduleId = 'data-v-' + hash(isProduction ? (shortFilePath + '\n' + content) : shortFilePath)
const needCssSourceMap = !isProduction && sourceMap && options.cssSourceMap !== false

const descriptor = parse(
source,
fileName,
sourceRoot,
sourceMap,
needCssSourceMap
sourceMap
)

if (resourceQuery) {
const query = qs.parse(resourceQuery.slice(1))

// template
if (query.template != null) {
return compileTemplate(descriptor, this, moduleId)
}

// script
if (query.script != null) {
this.callback(
null,
descriptor.script.content,
descriptor.script.map
)
return
}

// styles
if (query.style != null && query.index != null) {
return descriptor.styles[query.index].content
}
// if the query has a type field, this is a language block request
// e.g. foo.vue?type=template&id=xxxxx
// and we will return early
const incomingQuery = qs.parse(resourceQuery.slice(1))
if (incomingQuery.type) {
return selectBlock(descriptor, loaderContext, incomingQuery)
}

// module id for scoped CSS & hot-reload
const shortFilePath = path
.relative(context, resourcePath)
.replace(/^(\.\.[\\\/])+/, '')
const id = hash(
isProduction
? (shortFilePath + '\n' + source)
: shortFilePath
)

// feature information
const hasScoped = descriptor.styles.some(s => s.scoped)
const templateAttrs = (descriptor.template && descriptor.template.attrs) || {}
const hasFunctional = templateAttrs.functional
const hasComment = templateAttrs.comments
const needsHotReload = (
!isServer &&
!isProduction &&
(descriptor.script || descriptor.template) &&
options.hotReload !== false
)

// template
let templateImport = `var render, staticRenderFns`
if (descriptor.template) {
const src = descriptor.template.src || resourcePath
const langQuery = getLangQuery(descriptor.template)
const idQuery = hasScoped ? `&id=${id}` : ``
const fnQuery = hasFunctional ? `&functional` : ``
const commentQuery = hasComment ? `&comment` : ``
const query = `?vue&type=template${idQuery}${langQuery}${fnQuery}${commentQuery}`
const request = stringifyRequest(src + query)
templateImport = `import { render, staticRenderFns } from ${request}`
}

// script
let scriptImport = `var script = {}`
if (descriptor.script) {
const src = descriptor.script.src || resourcePath
const langQuery = getLangQuery(descriptor.script, 'js')
const query = `?vue&type=script${langQuery}`
const request = stringifyRequest(src + query)
scriptImport = (
`import script from ${request}\n` +
`export * from ${request}` // support named exports
)
}

// styles
let styleImports = ``
if (descriptor.styles.length) {
styleImports = descriptor.styles.map((style, i) => {
const src = style.src || resourcePath
const langQuery = getLangQuery(style, 'css')
const scopedQuery = style.scoped ? `&scoped&id=${id}` : ``
const query = `?vue&type=style&index=${i}${langQuery}${scopedQuery}`
const request = stringifyRequest(src + query)
return `import style${i} from ${request}`
}).join('\n')
}

// assemble
return assemble(resourcePath, descriptor)
let code = `
${templateImport}
${scriptImport}
${styleImports}
import normalizer from ${stringifyRequest(`!${componentNormalizerPath}`)}
var component = normalizer(
script,
render,
staticRenderFns,
${hasFunctional ? `true` : `false`},
null, // TODO style injection
${JSON.stringify(id)},
${isServer ? JSON.stringify(hash(request)) : `null`}
${incomingQuery.shadow ? `,true` : ``}
)
`.trim()

if (descriptor.customBlocks && descriptor.customBlocks.length) {
// TODO custom blocks
}

if (!isProduction) {
code += `\ncomponent.options.__file = ${JSON.stringify(shortFilePath)}`
}

if (needsHotReload) {
// TODO hot reload
}

code += `\nexport default component.exports`
// console.log(code)
return code
}

function getLangQuery (block, fallback) {
const lang = block.lang || fallback
return lang ? `&lang=${lang}` : ``
}

module.exports.VueLoaderPlugin = plugin
4 changes: 2 additions & 2 deletions lib/parse.js
Expand Up @@ -6,7 +6,7 @@ const SourceMapGenerator = require('source-map').SourceMapGenerator
const splitRE = /\r?\n/g
const emptyRE = /^(?:\/\/)?\s*$/

module.exports = (content, filename, sourceRoot, needMap, needCSSMap) => {
module.exports = (content, filename, sourceRoot, needMap) => {
const cacheKey = hash(filename + content)
let output = cache.get(cacheKey)
if (output) return output
Expand All @@ -20,7 +20,7 @@ module.exports = (content, filename, sourceRoot, needMap, needCSSMap) => {
sourceRoot
)
}
if (needCSSMap && output.styles) {
if (output.styles) {
output.styles.forEach(style => {
if (!style.src) {
style.map = generateSourceMap(
Expand Down
40 changes: 40 additions & 0 deletions lib/pitch.js
@@ -0,0 +1,40 @@
const qs = require('querystring')
const loaderUtils = require('loader-utils')
const templateLoaderPath = require.resolve('./template-loader')
const stylePostLoaderPath = require.resolve('./style-post-loader')

module.exports = code => code

// This pitching loader is responsible for catching all src import requests
// from within vue files and transform it into appropriate requests
module.exports.pitch = function (remainingRequest) {
const query = qs.parse(this.resourceQuery.slice(1))
if (query.vue != null) {
// For Scoped CSS: inject style-post-loader before css-loader
if (query.type === `style` && query.scoped != null) {
const cssLoaderIndex = this.loaders.findIndex(l => /\/css-loader/.test(l.request))
if (cssLoaderIndex) {
const afterLoaders = this.loaders.slice(1, cssLoaderIndex + 1).map(l => l.request)
const beforeLoaders = this.loaders.slice(cssLoaderIndex + 1).map(l => l.request)
const request = '-!' + [
...afterLoaders,
stylePostLoaderPath,
...beforeLoaders,
this.resourcePath + this.resourceQuery
].join('!')
return `export * from ${loaderUtils.stringifyRequest(this, request)}`
}
}

// for templates: inject the template compiler
if (query.type === `template`) {
const beforeLoaders = this.loaders.slice(1).map(l => l.request)
const request = '-!' + [
templateLoaderPath,
...beforeLoaders,
this.resourcePath + this.resourceQuery
].join('!')
return `export * from ${loaderUtils.stringifyRequest(this, request)}`
}
}
}
100 changes: 100 additions & 0 deletions lib/plugin.js
@@ -0,0 +1,100 @@
const qs = require('querystring')
const RuleSet = require('webpack/lib/RuleSet')

// TODO handle vueRule with oneOf
module.exports = class VueLoaderPlugin {
apply (compiler) {
// get a hold of the raw rules
const rawRules = compiler.options.module.rules
// use webpack's RuleSet utility to normalize user rules
const rawNormalizedRules = new RuleSet(rawRules).rules

// find the rule that applies to vue files
const vueRuleIndex = rawRules.findIndex((rule, i) => {
return !rule.enforce && rawNormalizedRules[i].resource(`foo.vue`)
})
const vueRule = rawRules[vueRuleIndex]

if (!vueRule) {
throw new Error(
`VueLoaderPlugin Error: no matching rule for vue files are found.`
)
}

// find the normalized version of the vue rule
const normalizedVueRule = rawNormalizedRules[vueRuleIndex]

// get the normlized "use" for vue files
const normalizedVueUse = normalizedVueRule.use.map(cleanUse)

// get new rules without the vue rule
const baseRules = rawRules.filter(r => r !== vueRule)
const normalizedRules = rawNormalizedRules.filter(r => r !== normalizedVueRule)

// construct a new rule for vue file, with oneOf containing
// multiple rules with dynamic resourceQuery functions that applies to
// different language blocks in a raw vue file.
const constructedRule = {
test: /\.vue$/,
oneOf: baseRules.map((rule, i) => {
// for each user rule, create a cloned rule by checking if the rule
// matches the lang specified in the resourceQuery.
return cloneRule(rule, normalizedRules[i], normalizedVueUse)
}).concat(vueRule)
}

// replace the original vue rule with our new constructed rule.
rawRules.splice(vueRuleIndex, 1, constructedRule)

// inject global pitcher (responsible for injecting CSS post loader)
rawRules.unshift({
loader: require.resolve('./pitch')
})
}
}

function cloneRule (rule, normalizedRule, vueUse) {
if (rule.oneOf) {
return Object.assign({}, rule, {
oneOf: rule.oneOf.map((r, i) => cloneRule(r, normalizedRule.oneOf[i]))
})
}

// Assuming `test` and `resourceQuery` tests are executed in series and
// synchronously (which is true based on RuleSet's implementation), we can
// save the current resource being matched from `test` so that we can access
// it in `resourceQuery`. This ensures when we use the normalized rule's
// resource check, include/exclude are matched correctly.
let currentResource
const res = Object.assign({}, rule, {
test: resource => {
currentResource = resource
return true
},
resourceQuery: query => {
const parsed = qs.parse(query.slice(1))
if (parsed.lang == null) {
return false
}
return normalizedRule.resource(`${currentResource}.${parsed.lang}`)
},
use: [
...(normalizedRule.use || []).map(cleanUse),
...vueUse
]
})

// delete shorthand since we have normalized use
delete res.loader
delete res.options

return res
}

// "ident" is exposed on normalized uses, delete in case it
// interferes with another normalization
function cleanUse (use) {
const res = Object.assign({}, use)
delete res.ident
return res
}

0 comments on commit 3175633

Please sign in to comment.