/
index.js
171 lines (138 loc) · 4.7 KB
/
index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
/* eslint-disable filenames/match-exported */
import crypto from "crypto"
import { basename, dirname, extname, relative, sep } from "path"
import appRoot from "app-root-dir"
import basex from "base-x"
import json5 from "json5"
const base62 = basex("0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
const root = appRoot.get()
const DEFAULT_LENGTH = 5
function hashString(input, precision = DEFAULT_LENGTH) {
return base62
.encode(
crypto
.createHash("sha256")
.update(input)
.digest()
)
.slice(0, precision)
}
function collectImportCallPaths(startPath) {
const imports = []
startPath.traverse({
Import: function Import(importPath) {
imports.push(importPath)
}
})
return imports
}
function getImportArgPath(path) {
return path.parentPath.get("arguments")[0]
}
function getSimplifiedPrefix(request) {
let simplified = request.replace(/^[./]+|(\.js$)/g, "")
if (simplified.endsWith("/")) {
simplified = `${simplified
.slice(0, -1)
.split("/")
.pop()}-`
} else {
simplified = ""
}
return simplified
}
const visited = Symbol("visited")
function processImport(path, state) {
if (path[visited]) {
return
}
path[visited] = true
const importArg = getImportArgPath(path)
const importArgNode = importArg.node
const { quasis, expressions, leadingComments } = importArgNode
const requester = dirname(state.file.opts.filename)
// Use only first part of template string as request part.
// In theory we could use all values in `quasis` but this often does not
// offer any benefit and just makes the resulting names longer.
const request = quasis ? quasis[0].value.cooked : importArgNode.value
// There exists the possibility of non usable value. Typically only
// when the user has import() statements with other complex data, but
// not a plain string or template string. We handle this gracefully by ignoring.
if (request == null) {
return
}
const jsonContent = {}
// Try to parse all previous comments
if (leadingComments) {
leadingComments.forEach((comment, index) => {
// Skip empty comments
if (!comment.value.trim()) {
return
}
// Webpack magic comments are declared as JSON5 but miss the curly braces.
let parsed
try {
parsed = json5.parse(`{${comment.value}}`)
} catch (err) {
// Most probably a non JSON5 comment
return
}
// Skip comment processing if it already contains a chunk name
if (parsed.webpackChunkName) {
jsonContent.webpackChunkName = true
return
}
// We copy over all fields and...
for (const key in parsed) {
jsonContent[key] = parsed[key]
}
// Cleanup the parsed comment afterwards
comment.value = ""
})
}
if (!jsonContent.webpackChunkName) {
const hasExpressions = expressions && expressions.length > 0
// Append [request] as placeholder for dynamic part in WebpackChunkName
const fullRequest = hasExpressions ? `${request}[request]` : request
// Prepend some clean identifier of the static part when using expressions.
// This is not required to work, but helps users to identify different chunks.
const requestPrefix = hasExpressions ? getSimplifiedPrefix(request) : ""
// Cleanup combined request to not contain any paths info
const plainRequest = basename(fullRequest, extname(fullRequest))
// Normalize requester between different OSs
const normalizedRequester = relative(root, requester)
.split(sep)
.join("/")
// Hash request origin and request
const importHash = hashString(`${normalizedRequester}::${request}`)
// Add our chunk name to the previously parsed values
jsonContent.webpackChunkName = `${requestPrefix}${plainRequest}-${importHash}`
// Convert to string and remove outer JSON object symbols {}
const magicComment = json5.stringify(jsonContent).slice(1, -1)
// Add as a new leading comment
importArg.addComment("leading", magicComment)
}
}
export default function smartWebpackImport({ types, template }) {
return {
name: "smart-webpack-import",
visitor: {
CallExpression(path, state) {
const imports = collectImportCallPaths(path)
imports.forEach((importCall) => processImport(importCall, state))
}
}
}
}
export function shouldPrintComment(comment) {
// Keep pure function markers which are generated by some plugins
// See sideEffects option: https://github.com/mishoo/UglifyJS2
if ((/[#@]__PURE__/).exec(comment)) {
return true
}
// Keep JSON5 magic comments used for Webpack hints
if ((/^\s?webpack[A-Z][A-Za-z]+:/).exec(comment)) {
return true
}
return false
}