UNPKG

4.98 kBJavaScriptView Raw
1const URL = require('url');
2const path = require('path');
3const fs = require('./utils/fs');
4const objectHash = require('./utils/objectHash');
5const md5 = require('./utils/md5');
6const isURL = require('./utils/is-url');
7const config = require('./utils/config');
8
9let ASSET_ID = 1;
10
11/**
12 * An Asset represents a file in the dependency tree. Assets can have multiple
13 * parents that depend on it, and can be added to multiple output bundles.
14 * The base Asset class doesn't do much by itself, but sets up an interface
15 * for subclasses to implement.
16 */
17class Asset {
18 constructor(name, pkg, options) {
19 this.id = ASSET_ID++;
20 this.name = name;
21 this.basename = path.basename(this.name);
22 this.relativeName = path.relative(options.rootDir, this.name);
23 this.package = pkg || {};
24 this.options = options;
25 this.encoding = 'utf8';
26 this.type = path.extname(this.name).slice(1);
27
28 this.processed = false;
29 this.contents = options.rendition ? options.rendition.value : null;
30 this.ast = null;
31 this.generated = null;
32 this.hash = null;
33 this.parentDeps = new Set();
34 this.dependencies = new Map();
35 this.depAssets = new Map();
36 this.parentBundle = null;
37 this.bundles = new Set();
38 this.cacheData = {};
39 this.buildTime = 0;
40 this.bundledSize = 0;
41 }
42
43 shouldInvalidate() {
44 return false;
45 }
46
47 async loadIfNeeded() {
48 if (this.contents == null) {
49 this.contents = await this.load();
50 }
51 }
52
53 async parseIfNeeded() {
54 await this.loadIfNeeded();
55 if (!this.ast) {
56 this.ast = await this.parse(this.contents);
57 }
58 }
59
60 async getDependencies() {
61 if (
62 this.options.rendition &&
63 this.options.rendition.hasDependencies === false
64 ) {
65 return;
66 }
67
68 await this.loadIfNeeded();
69
70 if (this.contents && this.mightHaveDependencies()) {
71 await this.parseIfNeeded();
72 await this.collectDependencies();
73 }
74 }
75
76 addDependency(name, opts) {
77 this.dependencies.set(name, Object.assign({name}, opts));
78 }
79
80 addURLDependency(url, from = this.name, opts) {
81 if (!url || isURL(url)) {
82 return url;
83 }
84
85 if (typeof from === 'object') {
86 opts = from;
87 from = this.name;
88 }
89
90 const parsed = URL.parse(url);
91 const resolved = path.resolve(path.dirname(from), parsed.pathname);
92 this.addDependency(
93 './' + path.relative(path.dirname(this.name), resolved),
94 Object.assign({dynamic: true}, opts)
95 );
96
97 parsed.pathname = this.options.parser
98 .getAsset(resolved, this.package, this.options)
99 .generateBundleName();
100
101 return URL.format(parsed);
102 }
103
104 async getConfig(filenames, opts = {}) {
105 // Resolve the config file
106 let conf = await config.resolve(opts.path || this.name, filenames);
107 if (conf) {
108 // Add as a dependency so it is added to the watcher and invalidates
109 // this asset when the config changes.
110 this.addDependency(conf, {includedInParent: true});
111 if (opts.load === false) {
112 return conf;
113 }
114
115 return await config.load(opts.path || this.name, filenames);
116 }
117
118 return null;
119 }
120
121 mightHaveDependencies() {
122 return true;
123 }
124
125 async load() {
126 return await fs.readFile(this.name, this.encoding);
127 }
128
129 parse() {
130 // do nothing by default
131 }
132
133 collectDependencies() {
134 // do nothing by default
135 }
136
137 async pretransform() {
138 // do nothing by default
139 }
140
141 async transform() {
142 // do nothing by default
143 }
144
145 async generate() {
146 return {
147 [this.type]: this.contents
148 };
149 }
150
151 async process() {
152 if (!this.generated) {
153 await this.loadIfNeeded();
154 await this.pretransform();
155 await this.getDependencies();
156 await this.transform();
157 this.generated = await this.generate();
158 this.hash = await this.generateHash();
159 }
160
161 return this.generated;
162 }
163
164 async postProcess(generated) {
165 return generated;
166 }
167
168 generateHash() {
169 return objectHash(this.generated);
170 }
171
172 invalidate() {
173 this.processed = false;
174 this.contents = null;
175 this.ast = null;
176 this.generated = null;
177 this.hash = null;
178 this.dependencies.clear();
179 this.depAssets.clear();
180 }
181
182 invalidateBundle() {
183 this.parentBundle = null;
184 this.bundles.clear();
185 this.parentDeps.clear();
186 }
187
188 generateBundleName() {
189 // Generate a unique name. This will be replaced with a nicer
190 // name later as part of content hashing.
191 return md5(this.name) + '.' + this.type;
192 }
193
194 replaceBundleNames(bundleNameMap) {
195 for (let key in this.generated) {
196 let value = this.generated[key];
197 if (typeof value === 'string') {
198 // Replace temporary bundle names in the output with the final content-hashed names.
199 for (let [name, map] of bundleNameMap) {
200 value = value.split(name).join(map);
201 }
202
203 this.generated[key] = value;
204 }
205 }
206 }
207
208 generateErrorMessage(err) {
209 return err;
210 }
211}
212
213module.exports = Asset;