UNPKG

7.07 kBJavaScriptView Raw
1const URL = require('url');
2const path = require('path');
3const clone = require('clone');
4const fs = require('@parcel/fs');
5const md5 = require('./utils/md5');
6const isURL = require('./utils/is-url');
7const config = require('./utils/config');
8const syncPromise = require('./utils/syncPromise');
9const logger = require('@parcel/logger');
10const Resolver = require('./Resolver');
11const objectHash = require('./utils/objectHash');
12
13/**
14 * An Asset represents a file in the dependency tree. Assets can have multiple
15 * parents that depend on it, and can be added to multiple output bundles.
16 * The base Asset class doesn't do much by itself, but sets up an interface
17 * for subclasses to implement.
18 */
19class Asset {
20 constructor(name, options) {
21 this.id = null;
22 this.name = name;
23 this.basename = path.basename(this.name);
24 this.relativeName = path
25 .relative(options.rootDir, this.name)
26 .replace(/\\/g, '/');
27 this.options = options;
28 this.encoding = 'utf8';
29 this.type = path.extname(this.name).slice(1);
30 this.hmrPageReload = false;
31
32 this.processed = false;
33 this.contents = options.rendition ? options.rendition.value : null;
34 this.ast = null;
35 this.generated = null;
36 this.hash = null;
37 this.sourceMaps = null;
38 this.parentDeps = new Set();
39 this.dependencies = new Map();
40 this.depAssets = new Map();
41 this.parentBundle = null;
42 this.bundles = new Set();
43 this.cacheData = {};
44 this.startTime = 0;
45 this.endTime = 0;
46 this.buildTime = 0;
47 this.bundledSize = 0;
48 this.resolver = new Resolver(options);
49 }
50
51 shouldInvalidate() {
52 return false;
53 }
54
55 async loadIfNeeded() {
56 if (this.contents == null) {
57 this.contents = await this.load();
58 }
59 }
60
61 async parseIfNeeded() {
62 await this.loadIfNeeded();
63 if (!this.ast) {
64 this.ast = await this.parse(this.contents);
65 }
66 }
67
68 async getDependencies() {
69 if (
70 this.options.rendition &&
71 this.options.rendition.hasDependencies === false
72 ) {
73 return;
74 }
75
76 await this.loadIfNeeded();
77
78 if (this.contents && this.mightHaveDependencies()) {
79 await this.parseIfNeeded();
80 await this.collectDependencies();
81 }
82 }
83
84 addDependency(name, opts) {
85 this.dependencies.set(name, Object.assign({name}, opts));
86 }
87
88 resolveDependency(url, from = this.name) {
89 const parsed = URL.parse(url);
90 let depName;
91 let resolved;
92 let dir = path.dirname(from);
93 const filename = decodeURIComponent(parsed.pathname);
94
95 if (filename[0] === '~' || filename[0] === '/') {
96 if (dir === '.') {
97 dir = this.options.rootDir;
98 }
99 depName = resolved = this.resolver.resolveFilename(filename, dir);
100 } else {
101 resolved = path.resolve(dir, filename);
102 depName = './' + path.relative(path.dirname(this.name), resolved);
103 }
104
105 return {depName, resolved};
106 }
107
108 addURLDependency(url, from = this.name, opts) {
109 if (!url || isURL(url)) {
110 return url;
111 }
112
113 if (typeof from === 'object') {
114 opts = from;
115 from = this.name;
116 }
117
118 const {depName, resolved} = this.resolveDependency(url, from);
119
120 this.addDependency(depName, Object.assign({dynamic: true, resolved}, opts));
121
122 const parsed = URL.parse(url);
123 parsed.pathname = this.options.parser
124 .getAsset(resolved, this.options)
125 .generateBundleName();
126
127 return URL.format(parsed);
128 }
129
130 get package() {
131 logger.warn(
132 '`asset.package` is deprecated. Please use `await asset.getPackage()` instead.'
133 );
134 return syncPromise(this.getPackage());
135 }
136
137 async getPackage() {
138 if (!this._package) {
139 this._package = await this.resolver.findPackage(path.dirname(this.name));
140 }
141
142 return this._package;
143 }
144
145 async getConfig(filenames, opts = {}) {
146 if (opts.packageKey) {
147 let pkg = await this.getPackage();
148 if (pkg && pkg[opts.packageKey]) {
149 return clone(pkg[opts.packageKey]);
150 }
151 }
152
153 // Resolve the config file
154 let conf = await config.resolve(opts.path || this.name, filenames);
155 if (conf) {
156 // Add as a dependency so it is added to the watcher and invalidates
157 // this asset when the config changes.
158 this.addDependency(conf, {includedInParent: true});
159 if (opts.load === false) {
160 return conf;
161 }
162
163 return config.load(opts.path || this.name, filenames);
164 }
165
166 return null;
167 }
168
169 mightHaveDependencies() {
170 return true;
171 }
172
173 async load() {
174 return fs.readFile(this.name, this.encoding);
175 }
176
177 parse() {
178 // do nothing by default
179 }
180
181 collectDependencies() {
182 // do nothing by default
183 }
184
185 async pretransform() {
186 // do nothing by default
187 }
188
189 async transform() {
190 // do nothing by default
191 }
192
193 async generate() {
194 return {
195 [this.type]: this.contents
196 };
197 }
198
199 async process() {
200 // Generate the id for this asset, unless it has already been set.
201 // We do this here rather than in the constructor to avoid unnecessary work in the main process.
202 // In development, the id is just the relative path to the file, for easy debugging and performance.
203 // In production, we use a short hash of the relative path.
204 if (!this.id) {
205 this.id =
206 this.options.production || this.options.scopeHoist
207 ? md5(this.relativeName, 'base64').slice(0, 4)
208 : this.relativeName;
209 }
210
211 if (!this.generated) {
212 await this.loadIfNeeded();
213 await this.pretransform();
214 await this.getDependencies();
215 await this.transform();
216 this.generated = await this.generate();
217 }
218
219 return this.generated;
220 }
221
222 async postProcess(generated) {
223 return generated;
224 }
225
226 generateHash() {
227 return objectHash(this.generated);
228 }
229
230 invalidate() {
231 this.processed = false;
232 this.contents = null;
233 this.ast = null;
234 this.generated = null;
235 this.hash = null;
236 this.dependencies.clear();
237 this.depAssets.clear();
238 }
239
240 invalidateBundle() {
241 this.parentBundle = null;
242 this.bundles.clear();
243 this.parentDeps.clear();
244 }
245
246 generateBundleName() {
247 // Generate a unique name. This will be replaced with a nicer
248 // name later as part of content hashing.
249 return md5(this.relativeName) + '.' + this.type;
250 }
251
252 replaceBundleNames(bundleNameMap) {
253 let copied = false;
254 for (let key in this.generated) {
255 let value = this.generated[key];
256 if (typeof value === 'string') {
257 // Replace temporary bundle names in the output with the final content-hashed names.
258 let newValue = value;
259 for (let [name, map] of bundleNameMap) {
260 newValue = newValue.split(name).join(map);
261 }
262
263 // Copy `this.generated` on write so we don't end up writing the final names to the cache.
264 if (newValue !== value && !copied) {
265 this.generated = Object.assign({}, this.generated);
266 copied = true;
267 }
268
269 this.generated[key] = newValue;
270 }
271 }
272 }
273
274 generateErrorMessage(err) {
275 return err;
276 }
277}
278
279module.exports = Asset;