tools/isobuild/compiler-deprecated-compile-step.js
// This file contains an old definition of CompileStep, an object that is passed
// to the package-provided file handler.
// Since then, the newer API called "Batch Plugins" have replaced it but we keep
// the functionality for the backwards-compatibility.
// @deprecated
// XXX COMPAT WITH 1.1.0.2
var _ = require('underscore');
var archinfo = require('../utils/archinfo');
var buildmessage = require('../utils/buildmessage.js');
var files = require('../fs/files');
var colonConverter = require('../utils/colon-converter.js');
var watch = require('../fs/watch');
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);
};
exports.makeCompileStep = function (sourceItem, file, inputSourceArch, options) {
var resources = options.resources;
var addAsset = options.addAsset;
var relPath = sourceItem.relPath;
var fileOptions = _.clone(sourceItem.fileOptions) || {};
var absPath = files.pathResolve(inputSourceArch.pkg.sourceRoot, relPath);
var filename = files.pathBasename(relPath);
var hash = file.hash;
var contents = file.contents;
// 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: 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;
},
_getOption(name, options) {
if (options && _.has(options, name)) {
return options[name];
}
const fileOptions = this.fileOptions;
return fileOptions && fileOptions[name];
},
/**
* @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: Buffer.from(files.convertToStandardLineEndings(options.data), 'utf8'),
lazy: this._getOption("lazy", options),
});
},
/**
* @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: Buffer.from(files.convertToStandardLineEndings(options.data), 'utf8'),
servePath: colonConverter.convert(
files.pathJoin(
inputSourceArch.pkg.serveRoot,
files.convertToStandardPath(options.path, true))),
sourceMap: convertSourceMapPaths(options.sourceMap,
files.convertToStandardPath),
lazy: this._getOption("lazy", options),
});
},
/**
* @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");
}
let sourcePath = this.inputPath;
if (_.has(options, "sourcePath") &&
typeof options.sourcePath === "string") {
sourcePath = options.sourcePath;
}
const targetPath = options.path || sourcePath;
if (typeof sourcePath !== "string") {
throw new Error("'sourcePath' option must be supplied to addJavaScript. Consider passing inputPath.");
}
var data = Buffer.from(
files.convertToStandardLineEndings(options.data), 'utf8');
resources.push({
type: "js",
data: data,
sourcePath,
targetPath,
servePath: colonConverter.convert(
files.pathJoin(
inputSourceArch.pkg.serveRoot,
files.convertToStandardPath(targetPath, true))),
hash: watch.sha1(data),
sourceMap: convertSourceMapPaths(options.sourceMap,
files.convertToStandardPath),
lazy: this._getOption("lazy", options),
bare: !! this._getOption("bare", options),
mainModule: !! this._getOption("mainModule", options),
});
},
/**
* @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 = Buffer.from(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) {
let sourcePath = this.inputPath;
if (_.has(options, "sourcePath") &&
typeof options.sourcePath === "string") {
sourcePath = options.sourcePath;
}
const message = options.message || ("error building " + relPath);
const info = { file: sourcePath };
if (options.line) info.line = options.line;
if (options.column) info.column = options.column;
if (options.func) info.func = options.func;
if (compileStep._getOption("lazy") === true) {
// Because this file is lazy, it might not have been explicitly
// added in package.js, so we should ignore compilation errors
// until and unless it is ever actually imported.
resources.push({
type: "js",
sourcePath,
targetPath: sourcePath,
servePath: sourcePath,
data: Buffer.from(
"throw new Error(" + JSON.stringify(message) + ");\n",
"utf8"),
lazy: true,
error: { message, info },
});
return;
}
buildmessage.error(message, info);
}
};
return compileStep;
};