Files
meteor/tools/compiler.js
Avital Oliver 358d797373 Improve error on publish-for-arch with colons
It's unfortunate that this change is in `compiler.compile`. It's
actually unfortunate that we only catch this during publish-for-arch.
But it seems too much to disallow all packages published with colons
even if they don't have binary dependencies since a lot of old
packages were created with our command-line tool that created
filenames with colons in them.
2015-03-26 18:46:12 -07:00

915 lines
35 KiB
JavaScript

var _ = require('underscore');
var archinfo = require('./archinfo.js');
var buildmessage = require('./buildmessage.js');
var bundler = require('./bundler.js');
var isopack = require('./isopack.js');
var isopackets = require('./isopackets.js');
var linker = require('./linker.js');
var meteorNpm = require('./meteor-npm.js');
var watch = require('./watch.js');
var Console = require('./console.js').Console;
var files = require('./files.js');
var colonConverter = require('./colon-converter.js');
var compiler = exports;
// Whenever you change anything about the code that generates isopacks, bump
// this version number. The idea is that the "format" field of the isopack
// JSON file only changes when the actual specified structure of the
// isopack/unibuild changes, but this version (which is build-tool-specific)
// can change when the the contents (not structure) of the built output
// changes. So eg, if we improve the linker's static analysis, this should be
// bumped.
//
// You should also update this whenever you update any of the packages used
// directly by the isopack creation process (eg js-analyze) since they do not
// end up as watched dependencies. (At least for now, packages only used in
// target creation (eg minifiers) don't require you to update BUILT_BY, though
// you will need to quit and rerun "meteor run".)
compiler.BUILT_BY = 'meteor/16';
// This is a list of all possible architectures that a build can target. (Client
// is expanded into 'web.browser' and 'web.cordova')
compiler.ALL_ARCHES = [ "os", "web.browser", "web.cordova" ];
compiler.compile = function (packageSource, options) {
buildmessage.assertInCapture();
var packageMap = options.packageMap;
var isopackCache = options.isopackCache;
var includeCordovaUnibuild = options.includeCordovaUnibuild;
var pluginWatchSet = packageSource.pluginWatchSet.clone();
var plugins = {};
var pluginProviderPackageNames = {};
// Build plugins
_.each(packageSource.pluginInfo, function (info) {
buildmessage.enterJob({
title: "building plugin `" + info.name +
"` in package `" + packageSource.name + "`",
rootPath: packageSource.sourceRoot
}, function () {
// XXX we should probably also pass options.noLineNumbers into
// buildJsImage so it can pass it back to its call to
// compiler.compile
var buildResult = bundler.buildJsImage({
name: info.name,
packageMap: packageMap,
isopackCache: isopackCache,
use: info.use,
sourceRoot: packageSource.sourceRoot,
sources: info.sources,
npmDependencies: info.npmDependencies,
// Plugins have their own npm dependencies separate from the
// rest of the package, so they need their own separate npm
// shrinkwrap and cache state.
npmDir: files.pathResolve(
files.pathJoin(packageSource.sourceRoot, '.npm', 'plugin', info.name))
});
if (buildmessage.jobHasMessages())
return;
_.each(buildResult.usedPackageNames, function (packageName) {
pluginProviderPackageNames[packageName] = true;
});
// Add this plugin's dependencies to our "plugin dependency"
// WatchSet. buildResult.watchSet will end up being the merged
// watchSets of all of the unibuilds of the plugin -- plugins have
// only one unibuild and this should end up essentially being just
// the source files of the plugin.
pluginWatchSet.merge(buildResult.watchSet);
// Register the built plugin's code.
if (!_.has(plugins, info.name))
plugins[info.name] = {};
plugins[info.name][buildResult.image.arch] = buildResult.image;
});
});
// Grab any npm dependencies. Keep them in a cache in the package
// source directory so we don't have to do this from scratch on
// every build.
//
// Go through a specialized npm dependencies update process,
// ensuring we don't get new versions of any (sub)dependencies. This
// process also runs mostly safely multiple times in parallel (which
// could happen if you have two apps running locally using the same
// package).
//
// We run this even if we have no dependencies, because we might
// need to delete dependencies we used to have.
var isPortable = true;
var nodeModulesPath = null;
if (packageSource.npmCacheDirectory) {
if (meteorNpm.updateDependencies(packageSource.name,
packageSource.npmCacheDirectory,
packageSource.npmDependencies)) {
nodeModulesPath = files.pathJoin(packageSource.npmCacheDirectory,
'node_modules');
if (! meteorNpm.dependenciesArePortable(packageSource.npmCacheDirectory))
isPortable = false;
}
}
var isopk = new isopack.Isopack;
isopk.initFromOptions({
name: packageSource.name,
metadata: packageSource.metadata,
version: packageSource.version,
isTest: packageSource.isTest,
plugins: plugins,
pluginWatchSet: pluginWatchSet,
cordovaDependencies: packageSource.cordovaDependencies,
npmDiscards: packageSource.npmDiscards,
includeTool: packageSource.includeTool,
debugOnly: packageSource.debugOnly
});
_.each(packageSource.architectures, function (unibuild) {
if (unibuild.arch === 'web.cordova' && ! includeCordovaUnibuild)
return;
var unibuildResult = compileUnibuild({
isopack: isopk,
sourceArch: unibuild,
isopackCache: isopackCache,
nodeModulesPath: nodeModulesPath,
isPortable: isPortable,
noLineNumbers: options.noLineNumbers
});
_.extend(pluginProviderPackageNames,
unibuildResult.pluginProviderPackageNames);
});
if (options.includePluginProviderPackageMap) {
isopk.setPluginProviderPackageMap(
packageMap.makeSubsetMap(_.keys(pluginProviderPackageNames)));
}
return isopk;
};
// options.sourceArch is a SourceArch to compile. Process all source files
// through the appropriate handlers and run the prelink phase on any resulting
// JavaScript. Create a new Unibuild and add it to options.isopack.
//
// Returns a list of source files that were used in the compilation.
var compileUnibuild = function (options) {
buildmessage.assertInCapture();
var isopk = options.isopack;
var inputSourceArch = options.sourceArch;
var isopackCache = options.isopackCache;
var nodeModulesPath = options.nodeModulesPath;
var isPortable = options.isPortable;
var noLineNumbers = options.noLineNumbers;
var isApp = ! inputSourceArch.pkg.name;
var resources = [];
var js = [];
var pluginProviderPackageNames = {};
// The current package always is a plugin provider. (This also means we no
// longer need a buildOfPath entry in buildinfo.json.)
pluginProviderPackageNames[isopk.name] = true;
var watchSet = inputSourceArch.watchSet.clone();
// *** Determine and load active plugins
// XXX we used to include our own extensions only if we were the
// "use" role. now we include them everywhere because we don't have
// a special "use" role anymore. it's not totally clear to me what
// the correct behavior should be -- we need to resolve whether we
// think about extensions as being global to a package or particular
// to a unibuild.
// (there's also some weirdness here with handling implies, because
// the implies field is on the target unibuild, but we really only care
// about packages.)
var activePluginPackages = [isopk];
// We don't use plugins from weak dependencies, because the ability
// to compile a certain type of file shouldn't depend on whether or
// not some unrelated package in the target has a dependency. And we
// skip unordered dependencies, because it's not going to work to
// have circular build-time dependencies.
//
// eachUsedUnibuild takes care of pulling in implied dependencies for us (eg,
// templating from standard-app-packages).
//
// We pass archinfo.host here, not self.arch, because it may be more specific,
// and because plugins always have to run on the host architecture.
compiler.eachUsedUnibuild({
dependencies: inputSourceArch.uses,
arch: archinfo.host(),
isopackCache: isopackCache,
skipUnordered: true
// implicitly skip weak deps by not specifying acceptableWeakPackages option
}, function (unibuild) {
if (unibuild.pkg.name === isopk.name)
return;
pluginProviderPackageNames[unibuild.pkg.name] = true;
// If other package is built from source, then we need to rebuild this
// package if any file in the other package that could define a plugin
// changes.
watchSet.merge(unibuild.pkg.pluginWatchSet);
if (_.isEmpty(unibuild.pkg.plugins))
return;
activePluginPackages.push(unibuild.pkg);
});
activePluginPackages = _.uniq(activePluginPackages);
// *** Assemble the list of source file handlers from the plugins
var allHandlersWithPkgs = {};
var sourceExtensions = {}; // maps source extensions to isTemplate
sourceExtensions['js'] = false;
allHandlersWithPkgs['js'] = {
pkgName: null /* native handler */,
handler: function (compileStep) {
// This is a hardcoded handler for *.js files. Since plugins
// are written in JavaScript we have to start somewhere.
var options = {
data: compileStep.read().toString('utf8'),
path: compileStep.inputPath,
sourcePath: compileStep.inputPath,
_hash: compileStep._hash
};
if (compileStep.fileOptions.hasOwnProperty("bare")) {
options.bare = compileStep.fileOptions.bare;
} else if (compileStep.fileOptions.hasOwnProperty("raw")) {
// XXX eventually get rid of backward-compatibility "raw" name
// XXX COMPAT WITH 0.6.4
options.bare = compileStep.fileOptions.raw;
}
compileStep.addJavaScript(options);
}
};
_.each(activePluginPackages, function (otherPkg) {
_.each(otherPkg.getSourceHandlers(), function (sourceHandler, ext) {
// XXX comparing function text here seems wrong.
if (_.has(allHandlersWithPkgs, ext) &&
allHandlersWithPkgs[ext].handler.toString() !== sourceHandler.handler.toString()) {
buildmessage.error(
"conflict: two packages included in " +
(inputSourceArch.pkg.name || "the app") + ", " +
(allHandlersWithPkgs[ext].pkgName || "the app") + " and " +
(otherPkg.name || "the app") + ", " +
"are both trying to handle ." + ext);
// Recover by just going with the first handler we saw
return;
}
// Is this handler only registered for, say, "web", and we're building,
// say, "os"?
if (sourceHandler.archMatching &&
!archinfo.matches(inputSourceArch.arch, sourceHandler.archMatching)) {
return;
}
allHandlersWithPkgs[ext] = {
pkgName: otherPkg.name,
handler: sourceHandler.handler
};
sourceExtensions[ext] = !!sourceHandler.isTemplate;
});
});
// *** Determine source files
// Note: sourceExtensions does not include leading dots
// Note: the getSourcesFunc function isn't expected to add its
// source files to watchSet; rather, the watchSet is for other
// things that the getSourcesFunc consulted (such as directory
// listings or, in some hypothetical universe, control files) to
// determine its source files.
var sourceItems = inputSourceArch.getSourcesFunc(sourceExtensions, watchSet);
if (nodeModulesPath) {
// If this slice has node modules, we should consider the shrinkwrap file
// to be part of its inputs. (This is a little racy because there's no
// guarantee that what we read here is precisely the version that's used,
// but it's better than nothing at all.)
//
// Note that this also means that npm modules used by plugins will get
// this npm-shrinkwrap.json in their pluginDependencies (including for all
// packages that depend on us)! This is good: this means that a tweak to
// an indirect dependency of the coffee-script npm module used by the
// coffeescript package will correctly cause packages with *.coffee files
// to be rebuilt.
var shrinkwrapPath = nodeModulesPath.replace(
/node_modules$/, 'npm-shrinkwrap.json');
watch.readAndWatchFile(watchSet, shrinkwrapPath);
}
// *** Process each source file
var addAsset = function (contents, relPath, hash) {
// XXX hack
if (! inputSourceArch.pkg.name)
relPath = relPath.replace(/^(private|public)\//, '');
resources.push({
type: "asset",
data: contents,
path: relPath,
servePath: colonConverter.convert(
files.pathJoin(inputSourceArch.pkg.serveRoot, relPath)),
hash: hash
});
};
var convertSourceMapPaths = function (sourcemap, f) {
if (! sourcemap) {
// Don't try to convert it if it doesn't exist
return sourcemap;
}
var srcmap = JSON.parse(sourcemap);
srcmap.sources = _.map(srcmap.sources, f);
return JSON.stringify(srcmap);
};
_.each(sourceItems, function (source) {
var relPath = source.relPath;
var fileOptions = _.clone(source.fileOptions) || {};
var absPath = files.pathResolve(inputSourceArch.pkg.sourceRoot, relPath);
var filename = files.pathBasename(relPath);
// readAndWatchFileWithHash returns an object carrying a buffer with the
// file-contents. The buffer contains the original data of the file (no EOL
// transforms from the tools/files.js part).
var file = watch.readAndWatchFileWithHash(watchSet, absPath);
var contents = file.contents;
Console.nudge(true);
if (contents === null) {
// It really sucks to put this check here, since this isn't publish
// code...
if (source.relPath.match(/:/)) {
buildmessage.error(
"Couldn't build this package on Windows due to the following file " +
"with a colon -- " + source.relPath + ". Please rename and " +
"and re-publish the package.");
} else {
buildmessage.error("File not found: " + source.relPath);
}
// recover by ignoring
return;
}
// Find the handler for source files with this extension.
var handler = null;
if (! fileOptions.isAsset) {
var parts = filename.split('.');
for (var i = 0; i < parts.length; i++) {
var extension = parts.slice(i).join('.');
if (_.has(allHandlersWithPkgs, extension)) {
handler = allHandlersWithPkgs[extension].handler;
break;
}
}
}
if (! handler) {
// If we don't have an extension handler, serve this file as a
// static resource on the client, or ignore it on the server.
//
// XXX This is pretty confusing, especially if you've
// accidentally forgotten a plugin -- revisit?
addAsset(contents, relPath, file.hash);
return;
}
// This object is called a #CompileStep and it's the interface
// to plugins that define new source file handlers (eg,
// Coffeescript).
//
// Fields on CompileStep:
//
// - arch: the architecture for which we are building
// - inputSize: total number of bytes in the input file
// - inputPath: the filename and (relative) path of the input
// file, eg, "foo.js". We don't provide a way to get the full
// path because you're not supposed to read the file directly
// off of disk. Instead you should call read(). That way we
// can ensure that the version of the file that you use is
// exactly the one that is recorded in the dependency
// information.
// - pathForSourceMap: If this file is to be included in a source map,
// this is the name you should use for it in the map.
// - rootOutputPath: on web targets, for resources such as
// stylesheet and static assets, this is the root URL that
// will get prepended to the paths you pick for your output
// files so that you get your own namespace, for example
// '/packages/foo'. null on non-web targets
// - fileOptions: any options passed to "api.addFiles"; for
// use by the plugin. The built-in "js" plugin uses the "bare"
// option for files that shouldn't be wrapped in a closure.
// - declaredExports: An array of symbols exported by this unibuild, or null
// if it may not export any symbols (eg, test unibuilds). This is used by
// CoffeeScript to ensure that it doesn't close over those symbols, eg.
// - read(n): read from the input file. If n is given it should
// be an integer, and you will receive the next n bytes of the
// file as a Buffer. If n is omitted you get the rest of the
// file.
// - appendDocument({ section: "head", data: "my markup" })
// Browser targets only. Add markup to the "head" or "body"
// Web targets only. Add markup to the "head" or "body"
// section of the document.
// - addStylesheet({ path: "my/stylesheet.css", data: "my css",
// sourceMap: "stringified json sourcemap"})
// Web targets only. Add a stylesheet to the
// document. 'path' is a requested URL for the stylesheet that
// may or may not ultimately be honored. (Meteor will add
// appropriate tags to cause the stylesheet to be loaded. It
// will be subject to any stylesheet processing stages in
// effect, such as minification.)
// - addJavaScript({ path: "my/program.js", data: "my code",
// sourcePath: "src/my/program.js",
// bare: true })
// Add JavaScript code, which will be namespaced into this
// package's environment (eg, it will see only the exports of
// this package's imports), and which will be subject to
// minification and so forth. Again, 'path' is merely a hint
// that may or may not be honored. 'sourcePath' is the path
// that will be used in any error messages generated (eg,
// "foo.js:4:1: syntax error"). It must be present and should
// be relative to the project root. Typically 'inputPath' will
// do handsomely. "bare" means to not wrap the file in
// a closure, so that its vars are shared with other files
// in the module.
// - addAsset({ path: "my/image.png", data: Buffer })
// Add a file to serve as-is over HTTP (web targets) or
// to include as-is in the bundle (os targets).
// This time `data` is a Buffer rather than a string. For
// web targets, it will be served at the exact path you
// request (concatenated with rootOutputPath). For server
// targets, the file can be retrieved by passing path to
// Assets.getText or Assets.getBinary.
// - error({ message: "There's a problem in your source file",
// sourcePath: "src/my/program.ext", line: 12,
// column: 20, func: "doStuff" })
// Flag an error -- at a particular location in a source
// file, if you like (you can even indicate a function name
// to show in the error, like in stack traces). sourcePath,
// line, column, and func are all optional.
//
// XXX for now, these handlers must only generate portable code
// (code that isn't dependent on the arch, other than 'web'
// vs 'os') -- they can look at the arch that is provided
// but they can't rely on the running on that particular arch
// (in the end, an arch-specific unibuild will be emitted only if
// there are native node modules). Obviously this should
// change. A first step would be a setOutputArch() function
// analogous to what we do with native node modules, but maybe
// what we want is the ability to ask the plugin ahead of time
// how specific it would like to force unibuilds to be.
//
// XXX we handle encodings in a rather cavalier way and I
// suspect we effectively end up assuming utf8. We can do better
// than that!
//
// XXX addAsset probably wants to be able to set MIME type and
// also control any manifest field we deem relevant (if any)
//
// XXX Some handlers process languages that have the concept of
// include files. These are problematic because we need to
// somehow instrument them to get the names and hashs of all of
// the files that they read for dependency tracking purposes. We
// don't have an API for that yet, so for now we provide a
// workaround, which is that _fullInputPath contains the full
// absolute path to the input files, which allows such a plugin
// to set up its include search path. It's then on its own for
// registering dependencies (for now..)
//
// XXX in the future we should give plugins an easy and clean
// way to return errors (that could go in an overall list of
// errors experienced across all files)
var readOffset = 0;
/**
* The comments for this class aren't used to generate docs right now.
* The docs live in the GitHub Wiki at: https://github.com/meteor/meteor/wiki/CompileStep-API-for-Build-Plugin-Source-Handlers
* @class CompileStep
* @summary The object passed into Plugin.registerSourceHandler
* @global
*/
var compileStep = {
/**
* @summary The total number of bytes in the input file.
* @memberOf CompileStep
* @instance
* @type {Integer}
*/
inputSize: contents.length,
/**
* @summary The filename and relative path of the input file.
* Please don't use this filename to read the file from disk, instead
* use [compileStep.read](CompileStep-read).
* @type {String}
* @instance
* @memberOf CompileStep
*/
inputPath: files.convertToOSPath(relPath, true),
/**
* @summary The filename and absolute path of the input file.
* Please don't use this filename to read the file from disk, instead
* use [compileStep.read](CompileStep-read).
* @type {String}
* @instance
* @memberOf CompileStep
*/
fullInputPath: files.convertToOSPath(absPath),
// The below is used in the less and stylus packages... so it should be
// public API.
_fullInputPath: files.convertToOSPath(absPath), // avoid, see above..
// Used for one optimization. Don't rely on this otherwise.
_hash: file.hash,
// XXX duplicates _pathForSourceMap() in linker
/**
* @summary If you are generating a sourcemap for the compiled file, use
* this path for the original file in the sourcemap.
* @type {String}
* @memberOf CompileStep
* @instance
*/
pathForSourceMap: files.convertToOSPath(
inputSourceArch.pkg.name ? inputSourceArch.pkg.name + "/" + relPath :
files.pathBasename(relPath), true),
// null if this is an app. intended to be used for the sources
// dictionary for source maps.
/**
* @summary The name of the package in which the file being built exists.
* @type {String}
* @memberOf CompileStep
* @instance
*/
packageName: inputSourceArch.pkg.name,
/**
* @summary On web targets, this will be the root URL prepended
* to the paths you pick for your output files. For example,
* it could be "/packages/my-package".
* @type {String}
* @memberOf CompileStep
* @instance
*/
rootOutputPath: files.convertToOSPath(
inputSourceArch.pkg.serveRoot, true),
/**
* @summary The architecture for which we are building. Can be "os",
* "web.browser", or "web.cordova".
* @type {String}
* @memberOf CompileStep
* @instance
*/
arch: inputSourceArch.arch,
/**
* @deprecated in 0.9.4
* This is a duplicate API of the above, we don't need it.
*/
archMatches: function (pattern) {
return archinfo.matches(inputSourceArch.arch, pattern);
},
/**
* @summary Any options passed to "api.addFiles".
* @type {Object}
* @memberOf CompileStep
* @instance
*/
fileOptions: fileOptions,
/**
* @summary The list of exports that the current package has defined.
* Can be used to treat those symbols differently during compilation.
* @type {Object}
* @memberOf CompileStep
* @instance
*/
declaredExports: _.pluck(inputSourceArch.declaredExports, 'name'),
/**
* @summary Read from the input file. If `n` is specified, returns the
* next `n` bytes of the file as a Buffer. XXX not sure if this actually
* returns a String sometimes...
* @param {Integer} [n] The number of bytes to return.
* @instance
* @memberOf CompileStep
* @returns {Buffer}
*/
read: function (n) {
if (n === undefined || readOffset + n > contents.length)
n = contents.length - readOffset;
var ret = contents.slice(readOffset, readOffset + n);
readOffset += n;
return ret;
},
/**
* @summary Works in web targets only. Add markup to the `head` or `body`
* section of the document.
* @param {Object} options
* @param {String} options.section Which section of the document should
* be appended to. Can only be "head" or "body".
* @param {String} options.data The content to append.
* @memberOf CompileStep
* @instance
*/
addHtml: function (options) {
if (! archinfo.matches(inputSourceArch.arch, "web"))
throw new Error("Document sections can only be emitted to " +
"web targets");
if (options.section !== "head" && options.section !== "body")
throw new Error("'section' must be 'head' or 'body'");
if (typeof options.data !== "string")
throw new Error("'data' option to appendDocument must be a string");
resources.push({
type: options.section,
data: new Buffer(files.convertToStandardLineEndings(options.data), 'utf8')
});
},
/**
* @deprecated in 0.9.4
*/
appendDocument: function (options) {
this.addHtml(options);
},
/**
* @summary Web targets only. Add a stylesheet to the document.
* @param {Object} options
* @param {String} path The requested path for the added CSS, may not be
* satisfied if there are path conflicts.
* @param {String} data The content of the stylesheet that should be
* added.
* @param {String} sourceMap A stringified JSON sourcemap, in case the
* stylesheet was generated from a different file.
* @memberOf CompileStep
* @instance
*/
addStylesheet: function (options) {
if (! archinfo.matches(inputSourceArch.arch, "web"))
throw new Error("Stylesheets can only be emitted to " +
"web targets");
if (typeof options.data !== "string")
throw new Error("'data' option to addStylesheet must be a string");
resources.push({
type: "css",
refreshable: true,
data: new Buffer(files.convertToStandardLineEndings(options.data), 'utf8'),
servePath: colonConverter.convert(
files.pathJoin(
inputSourceArch.pkg.serveRoot,
files.convertToStandardPath(options.path, true))),
sourceMap: convertSourceMapPaths(options.sourceMap,
files.convertToStandardPath)
});
},
/**
* @summary Add JavaScript code. The code added will only see the
* namespaces imported by this package as runtime dependencies using
* ['api.use'](#PackageAPI-use). If the file being compiled was added
* with the bare flag, the resulting JavaScript won't be wrapped in a
* closure.
* @param {Object} options
* @param {String} options.path The path at which the JavaScript file
* should be inserted, may not be honored in case of path conflicts.
* @param {String} options.data The code to be added.
* @param {String} options.sourcePath The path that will be used in
* any error messages generated by this file, e.g. `foo.js:4:1: error`.
* @memberOf CompileStep
* @instance
*/
addJavaScript: function (options) {
if (typeof options.data !== "string")
throw new Error("'data' option to addJavaScript must be a string");
if (typeof options.sourcePath !== "string")
throw new Error("'sourcePath' option must be supplied to addJavaScript. Consider passing inputPath.");
if (options.bare && ! archinfo.matches(inputSourceArch.arch, "web"))
throw new Error("'bare' option may only be used for web targets");
// By default, use fileOptions for the `bare` option but also allow
// overriding it with the options
var bare = fileOptions.bare;
if (options.hasOwnProperty("bare")) {
bare = options.bare;
}
js.push({
source: files.convertToStandardLineEndings(options.data),
sourcePath: files.convertToStandardPath(options.sourcePath, true),
servePath: files.pathJoin(
inputSourceArch.pkg.serveRoot,
files.convertToStandardPath(options.path, true)),
bare: !! bare,
sourceMap: convertSourceMapPaths(options.sourceMap,
files.convertToStandardPath),
sourceHash: options._hash
});
},
/**
* @summary Add a file to serve as-is to the browser or to include on
* the browser, depending on the target. On the web, it will be served
* at the exact path requested. For server targets, it can be retrieved
* using `Assets.getText` or `Assets.getBinary`.
* @param {Object} options
* @param {String} path The path at which to serve the asset.
* @param {Buffer|String} data The data that should be placed in
* the file.
* @memberOf CompileStep
* @instance
*/
addAsset: function (options) {
if (! (options.data instanceof Buffer)) {
if (_.isString(options.data)) {
options.data = new Buffer(options.data);
} else {
throw new Error("'data' option to addAsset must be a Buffer or String.");
}
}
addAsset(options.data, files.convertToStandardPath(options.path, true));
},
/**
* @summary Display a build error.
* @param {Object} options
* @param {String} message The error message to display.
* @param {String} [sourcePath] The path to display in the error message.
* @param {Integer} line The line number to display in the error message.
* @param {String} func The function name to display in the error message.
* @memberOf CompileStep
* @instance
*/
error: function (options) {
buildmessage.error(options.message || ("error building " + relPath), {
file: options.sourcePath,
line: options.line ? options.line : undefined,
column: options.column ? options.column : undefined,
func: options.func ? options.func : undefined
});
}
};
try {
(buildmessage.markBoundary(handler))(compileStep);
} catch (e) {
e.message = e.message + " (compiling " + relPath + ")";
buildmessage.exception(e);
// Recover by ignoring this source file (as best we can -- the
// handler might already have emitted resources)
}
});
// *** Run Phase 1 link
// Load jsAnalyze from the js-analyze package... unless we are the
// js-analyze package, in which case never mind. (The js-analyze package's
// default unibuild is not allowed to depend on anything!)
var jsAnalyze = null;
if (! _.isEmpty(js) && inputSourceArch.pkg.name !== "js-analyze") {
jsAnalyze = isopackets.load('js-analyze')['js-analyze'].JSAnalyze;
}
var results = linker.prelink({
inputFiles: js,
useGlobalNamespace: isApp,
// I was confused about this, so I am leaving a comment -- the
// combinedServePath is either [pkgname].js or [pluginName]:plugin.js.
// XXX: If we change this, we can get rid of source arch names!
combinedServePath: isApp ? null :
"/packages/" + colonConverter.convert(
inputSourceArch.pkg.name +
(inputSourceArch.kind === "main" ? "" : (":" + inputSourceArch.kind)) +
".js"),
name: inputSourceArch.pkg.name || null,
declaredExports: _.pluck(inputSourceArch.declaredExports, 'name'),
jsAnalyze: jsAnalyze,
noLineNumbers: noLineNumbers
});
// *** Determine captured variables
var packageVariables = [];
var packageVariableNames = {};
_.each(inputSourceArch.declaredExports, function (symbol) {
if (_.has(packageVariableNames, symbol.name))
return;
packageVariables.push({
name: symbol.name,
export: symbol.testOnly? "tests" : true
});
packageVariableNames[symbol.name] = true;
});
_.each(results.assignedVariables, function (name) {
if (_.has(packageVariableNames, name))
return;
packageVariables.push({
name: name
});
packageVariableNames[name] = true;
});
// *** Consider npm dependencies and portability
var arch = inputSourceArch.arch;
if (arch === "os" && ! isPortable) {
// Contains non-portable compiled npm modules, so set arch correctly
arch = archinfo.host();
}
if (! archinfo.matches(arch, "os")) {
// npm modules only work on server architectures
nodeModulesPath = undefined;
}
// *** Output unibuild object
isopk.addUnibuild({
kind: inputSourceArch.kind,
arch: arch,
uses: inputSourceArch.uses,
implies: inputSourceArch.implies,
watchSet: watchSet,
nodeModulesPath: nodeModulesPath,
prelinkFiles: results.files,
packageVariables: packageVariables,
resources: resources
});
return {
pluginProviderPackageNames: pluginProviderPackageNames
};
};
// Iterates over each in options.dependencies as well as unibuilds implied by
// them. The packages in question need to already be built and in
// options.isopackCache.
compiler.eachUsedUnibuild = function (
options, callback) {
buildmessage.assertInCapture();
var dependencies = options.dependencies;
var arch = options.arch;
var isopackCache = options.isopackCache;
var acceptableWeakPackages = options.acceptableWeakPackages || {};
var processedUnibuildId = {};
var usesToProcess = [];
_.each(dependencies, function (use) {
if (options.skipUnordered && use.unordered)
return;
if (use.weak && !_.has(acceptableWeakPackages, use.package))
return;
usesToProcess.push(use);
});
while (! _.isEmpty(usesToProcess)) {
var use = usesToProcess.shift();
var usedPackage = isopackCache.getIsopack(use.package);
// Ignore this package if we were told to skip debug-only packages and it is
// debug-only.
if (usedPackage.debugOnly && options.skipDebugOnly)
continue;
var unibuild = usedPackage.getUnibuildAtArch(arch);
if (!unibuild) {
// The package exists but there's no unibuild for us. A buildmessage has
// already been issued. Recover by skipping.
continue;
}
if (_.has(processedUnibuildId, unibuild.id))
continue;
processedUnibuildId[unibuild.id] = true;
callback(unibuild, {
unordered: !!use.unordered,
weak: !!use.weak
});
_.each(unibuild.implies, function (implied) {
usesToProcess.push(implied);
});
}
};