mirror of
https://github.com/meteor/meteor.git
synced 2026-05-02 03:01:46 -04:00
524 lines
17 KiB
JavaScript
524 lines
17 KiB
JavaScript
var archinfo = require('../utils/archinfo');
|
|
var buildmessage = require('../utils/buildmessage.js');
|
|
var files = require('../fs/files');
|
|
var _ = require('underscore');
|
|
import utils from '../utils/utils.js';
|
|
|
|
let nextId = 1;
|
|
|
|
exports.SourceProcessor = function (options) {
|
|
var self = this;
|
|
self.isopack = options.isopack;
|
|
self.extensions = (options.extensions || []).slice();
|
|
self.filenames = (options.filenames || []).slice();
|
|
self.archMatching = options.archMatching;
|
|
self.isTemplate = !! options.isTemplate;
|
|
self.factoryFunction = options.factoryFunction;
|
|
self.methodName = options.methodName;
|
|
self.id = `${ options.isopack.displayName() }#${ nextId++ }`;
|
|
self.userPlugin = null;
|
|
};
|
|
Object.assign(exports.SourceProcessor.prototype, {
|
|
// Call the user's factory function to get the actual build plugin object.
|
|
// Note that we're supposed to have one userPlugin per project, so this
|
|
// assumes that each Isopack object is specific to a project. We don't run
|
|
// this immediately on evaluating Plugin.registerCompiler; we instead wait
|
|
// until the whole plugin file has been evaluated (so that it can use things
|
|
// defined later in the file).
|
|
instantiatePlugin: async function () {
|
|
var self = this;
|
|
buildmessage.assertInCapture();
|
|
if (self.userPlugin) {
|
|
throw Error("Called instantiatePlugin twice?");
|
|
}
|
|
await buildmessage.enterJob(
|
|
`running ${self.methodName} callback in package ` +
|
|
self.isopack.displayName(),
|
|
async () => {
|
|
try {
|
|
const markedFactoryFunction = buildmessage.markBoundary(self.factoryFunction);
|
|
self.userPlugin = await markedFactoryFunction.call(null);
|
|
// If we have a disk cache directory and the plugin wants it, use it.
|
|
if (self.isopack.pluginCacheDir &&
|
|
self.userPlugin.setDiskCacheDirectory) {
|
|
await buildmessage.markBoundary(function () {
|
|
self.userPlugin.setDiskCacheDirectory(
|
|
files.convertToOSPath(self.isopack.pluginCacheDir)
|
|
);
|
|
})();
|
|
}
|
|
} catch (e) {
|
|
buildmessage.exception(e);
|
|
}
|
|
}
|
|
);
|
|
},
|
|
relevantForArch: function (arch) {
|
|
var self = this;
|
|
return ! self.archMatching || archinfo.matches(arch, self.archMatching);
|
|
}
|
|
});
|
|
|
|
// Represents a set of SourceProcessors available in a given package. They may
|
|
// not have conflicting extensions or filenames.
|
|
export class SourceProcessorSet {
|
|
constructor(myPackageDisplayName, {
|
|
hardcodeJs,
|
|
singlePackage,
|
|
allowConflicts,
|
|
} = {}) {
|
|
// For error messages only.
|
|
this._myPackageDisplayName = myPackageDisplayName;
|
|
// If this represents the SourceProcessors *registered* by a single package
|
|
// (vs those *available* to a package), use different error messages.
|
|
this._singlePackage = singlePackage;
|
|
// If this is being used for *compilers*, we hardcode *.js. If it is being
|
|
// used for linters, we don't.
|
|
this._hardcodeJs = !! hardcodeJs;
|
|
// Multiple linters may be registered on the same extension or filename, but
|
|
// not compilers.
|
|
this._allowConflicts = !! allowConflicts;
|
|
|
|
// Map from extension -> [SourceProcessor]
|
|
this._byExtension = {};
|
|
// Map from basename -> [SourceProcessor]
|
|
this._byFilename = {};
|
|
// This is just an duplicate-free list of all SourceProcessors in
|
|
// byExtension or byFilename.
|
|
this.allSourceProcessors = [];
|
|
// extension -> { handler, packageDisplayName, isTemplate, archMatching }
|
|
this._legacyHandlers = {};
|
|
}
|
|
|
|
_conflictError(package1, package2, conflict) {
|
|
if (this._singlePackage) {
|
|
buildmessage.error(
|
|
`plugins in package ${ this._myPackageDisplayName } define multiple ` +
|
|
`handlers for ${ conflict }`);
|
|
} else {
|
|
buildmessage.error(
|
|
`conflict: two packages included in ${ this._myPackageDisplayName } ` +
|
|
`(${ package1 } and ${ package2 }) are both trying to handle ` +
|
|
conflict);
|
|
}
|
|
}
|
|
|
|
addSourceProcessor(sp) {
|
|
buildmessage.assertInJob();
|
|
this._addSourceProcessorHelper(sp, sp.extensions, this._byExtension, '*.');
|
|
this._addSourceProcessorHelper(sp, sp.filenames, this._byFilename, '');
|
|
// If everything conflicted, then the SourceProcessors will be in
|
|
// allSourceProcessors but not any of the data structures, but in that case
|
|
// the caller should be checking for errors anyway.
|
|
this.allSourceProcessors.push(sp);
|
|
}
|
|
_addSourceProcessorHelper(sp, things, byThing, errorPrefix) {
|
|
buildmessage.assertInJob();
|
|
|
|
things.forEach((thing) => {
|
|
if (byThing.hasOwnProperty(thing)) {
|
|
if (this._allowConflicts) {
|
|
byThing[thing].push(sp);
|
|
} else {
|
|
this._conflictError(sp.isopack.displayName(),
|
|
byThing[thing][0].isopack.displayName(),
|
|
errorPrefix + thing);
|
|
// recover by ignoring this one
|
|
}
|
|
} else {
|
|
byThing[thing] = [sp];
|
|
}
|
|
});
|
|
}
|
|
|
|
addLegacyHandler({ extension, handler, packageDisplayName, isTemplate,
|
|
archMatching }) {
|
|
if (this._allowConflicts) {
|
|
throw Error("linters have no legacy handlers");
|
|
}
|
|
|
|
if (this._byExtension.hasOwnProperty(extension)) {
|
|
this._conflictError(packageDisplayName,
|
|
this._byExtension[extension].isopack.displayName(),
|
|
'*.' + extension);
|
|
// recover by ignoring
|
|
return;
|
|
}
|
|
if (this._legacyHandlers.hasOwnProperty(extension)) {
|
|
this._conflictError(packageDisplayName,
|
|
this._legacyHandlers[extension].packageDisplayName,
|
|
'*.' + extension);
|
|
// recover by ignoring
|
|
return;
|
|
}
|
|
this._legacyHandlers[extension] =
|
|
{handler, packageDisplayName, isTemplate, archMatching};
|
|
}
|
|
|
|
// Adds all the source processors (and legacy handlers) from the other set to
|
|
// this one. Logs buildmessage errors on conflict. Ignores packageDisplayName
|
|
// and singlePackage. If arch is set, skips SourceProcessors that
|
|
// don't match it.
|
|
merge(otherSet, options = {}) {
|
|
const { arch } = options;
|
|
buildmessage.assertInJob();
|
|
otherSet.allSourceProcessors.forEach((sourceProcessor) => {
|
|
if (! arch || sourceProcessor.relevantForArch(arch)) {
|
|
this.addSourceProcessor(sourceProcessor);
|
|
}
|
|
});
|
|
_.each(otherSet._legacyHandlers, (info, extension) => {
|
|
const { handler, packageDisplayName, isTemplate, archMatching } = info;
|
|
this.addLegacyHandler(
|
|
{extension, handler, packageDisplayName, isTemplate, archMatching});
|
|
});
|
|
}
|
|
|
|
// Note: Only returns SourceProcessors, not legacy handlers.
|
|
getByExtension(extension) {
|
|
if (this._allowConflicts) {
|
|
throw Error("Can't call getByExtension for linters");
|
|
}
|
|
|
|
if (this._byExtension.hasOwnProperty(extension)) {
|
|
return this._byExtension[extension][0];
|
|
}
|
|
return null;
|
|
}
|
|
|
|
// Note: Only returns SourceProcessors, not legacy handlers.
|
|
getByFilename(filename) {
|
|
if (this._allowConflicts) {
|
|
throw Error("Can't call getByFilename for linters");
|
|
}
|
|
|
|
if (this._byFilename.hasOwnProperty(filename)) {
|
|
return this._byFilename[filename][0];
|
|
}
|
|
return null;
|
|
}
|
|
|
|
// filename, arch -> SourceClassification
|
|
classifyFilename(filename, arch) {
|
|
// First check to see if a plugin registered for this exact filename.
|
|
if (this._byFilename.hasOwnProperty(filename)) {
|
|
return new SourceClassification('filename', {
|
|
arch,
|
|
sourceProcessors: this._byFilename[filename].slice()
|
|
});
|
|
}
|
|
|
|
if (filename === ".meteorignore") {
|
|
return new SourceClassification("meteor-ignore");
|
|
}
|
|
|
|
// Now check to see if a plugin registered for an extension. We prefer
|
|
// longer extensions.
|
|
const parts = filename.split('.');
|
|
// don't use iteration functions, so we can return (and start at #1)
|
|
for (let i = 1; i < parts.length; i++) {
|
|
const extension = parts.slice(i).join('.');
|
|
|
|
if (this._byExtension.hasOwnProperty(extension)) {
|
|
return new SourceClassification('extension', {
|
|
arch,
|
|
extension,
|
|
sourceProcessors: this._byExtension[extension]
|
|
});
|
|
}
|
|
|
|
if (this._hardcodeJs && extension === 'js') {
|
|
// If there is no special sourceProcessor for handling a .js file,
|
|
// we can still classify it as extension/js, only without any
|
|
// source processors. #HardcodeJs
|
|
return new SourceClassification('extension', {
|
|
extension,
|
|
usesDefaultSourceProcessor: true
|
|
});
|
|
}
|
|
|
|
if (this._legacyHandlers.hasOwnProperty(extension)) {
|
|
const legacy = this._legacyHandlers[extension];
|
|
if (legacy.archMatching &&
|
|
! archinfo.matches(arch, legacy.archMatching)) {
|
|
return new SourceClassification('wrong-arch');
|
|
}
|
|
return new SourceClassification('legacy-handler', {
|
|
extension,
|
|
legacyHandler: legacy.handler,
|
|
legacyIsTemplate: legacy.isTemplate
|
|
});
|
|
}
|
|
}
|
|
|
|
// Nothing matches; it must be a static asset (or a non-linted file).
|
|
return new SourceClassification('unmatched');
|
|
}
|
|
|
|
isEmpty() {
|
|
return _.isEmpty(this._byFilename) && _.isEmpty(this._byExtension) &&
|
|
_.isEmpty(this._legacyHandlers);
|
|
}
|
|
|
|
isConflictsAllowed() {
|
|
return this._allowConflicts;
|
|
}
|
|
|
|
// Returns an options object suitable for passing to
|
|
// `watch.readAndWatchDirectory` to find source files processed by this
|
|
// SourceProcessorSet.
|
|
appReadDirectoryOptions(arch) {
|
|
const include = [];
|
|
const names = [];
|
|
let addedJs = false;
|
|
|
|
function addExtension(ext) {
|
|
include.push(new RegExp('\\.' + utils.quotemeta(ext) + '$'));
|
|
if (ext === 'js') {
|
|
addedJs = true;
|
|
}
|
|
}
|
|
|
|
_.each(this._byExtension, (sourceProcessors, ext) => {
|
|
if (sourceProcessors.some(sp => sp.relevantForArch(arch))) {
|
|
addExtension(ext);
|
|
}
|
|
});
|
|
Object.keys(this._legacyHandlers).forEach(addExtension);
|
|
|
|
if (this._hardcodeJs && ! addedJs) {
|
|
// If there is no sourceProcessor for handling .js files, we still
|
|
// want to make sure they get picked up when we're reading the
|
|
// contents of app directories. #HardcodeJs
|
|
addExtension('js');
|
|
}
|
|
|
|
_.each(this._byFilename, (sourceProcessors, filename) => {
|
|
if (sourceProcessors.some(sp => sp.relevantForArch(arch))) {
|
|
names.push(filename);
|
|
}
|
|
});
|
|
return {include, names, exclude: []};
|
|
}
|
|
}
|
|
|
|
class SourceClassification {
|
|
constructor(type, {
|
|
legacyHandler,
|
|
extension,
|
|
sourceProcessors,
|
|
usesDefaultSourceProcessor,
|
|
legacyIsTemplate,
|
|
arch,
|
|
} = {}) {
|
|
const knownTypes = [
|
|
'extension',
|
|
'filename',
|
|
'legacy-handler',
|
|
'wrong-arch',
|
|
'unmatched',
|
|
'meteor-ignore',
|
|
];
|
|
if (knownTypes.indexOf(type) === -1) {
|
|
throw Error(`Unknown SourceClassification type ${ type }`);
|
|
}
|
|
// This is the only thing we can write to `this` before checking for
|
|
// wrong-arch.
|
|
this.type = type;
|
|
|
|
if (type === 'extension' || type === 'filename') {
|
|
if (sourceProcessors) {
|
|
if (! arch) {
|
|
throw Error("need to filter based on arch!");
|
|
}
|
|
|
|
// If there's a SourceProcessor (or legacy handler) registered for this
|
|
// file but not for this arch, we want to ignore it instead of
|
|
// processing it or treating it as a static asset. (Note that prior to
|
|
// the batch-plugins project, files added in a package with
|
|
// `api.addFiles('foo.bar')` where *.bar is a web-specific legacy
|
|
// handler (eg) would end up adding 'foo.bar' as a static asset on
|
|
// non-web programs, which was unintended. This didn't happen in apps
|
|
// because initFromAppDir's getFiles never added them.)
|
|
const filteredSourceProcessors = sourceProcessors.filter(
|
|
(sourceProcessor) => sourceProcessor.relevantForArch(arch)
|
|
);
|
|
if (! filteredSourceProcessors.length) {
|
|
// Wrong architecture! Rewrite this.type and return. (Note that we
|
|
// haven't written anything else to `this` so far.)
|
|
this.type = 'wrong-arch';
|
|
return;
|
|
}
|
|
|
|
this.sourceProcessors = filteredSourceProcessors;
|
|
} else if (!(type === 'extension' && extension === 'js')) {
|
|
// 'extension' and 'filename' classifications need to have at least one
|
|
// SourceProcessor, unless it's the #HardcodeJs special case.
|
|
throw Error(`missing sourceProcessors for ${ type }!`);
|
|
}
|
|
}
|
|
|
|
if (type === 'legacy-handler') {
|
|
if (! legacyHandler) {
|
|
throw Error('SourceClassification needs legacyHandler!');
|
|
}
|
|
if (legacyIsTemplate === undefined) {
|
|
throw Error('SourceClassification needs legacyIsTemplate!');
|
|
}
|
|
this.legacyHandler = legacyHandler;
|
|
this.legacyIsTemplate = legacyIsTemplate;
|
|
}
|
|
|
|
if (type === 'extension' || type === 'legacy-handler') {
|
|
if (! extension) {
|
|
throw Error('extension SourceClassification needs extension!');
|
|
}
|
|
this.extension = extension;
|
|
}
|
|
|
|
if (usesDefaultSourceProcessor) {
|
|
if (this.extension !== 'js' &&
|
|
this.extension !== 'css') {
|
|
// We only currently hard-code support for processing .js files
|
|
// when no source processor is registered (#HardcodeJs). Default
|
|
// support could conceivably be extended to .css files too, but
|
|
// anything else is almost certainly a mistake.
|
|
throw Error('non-JS/CSS file relying on default source processor?');
|
|
}
|
|
this.usesDefaultSourceProcessor = true;
|
|
} else {
|
|
this.usesDefaultSourceProcessor = false;
|
|
}
|
|
}
|
|
|
|
isNonLegacySource() {
|
|
return this.type === 'extension' || this.type === 'filename';
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// This is the base class of the object presented to the user's plugin code.
|
|
export class InputFile {
|
|
/**
|
|
* @summary Returns the full contents of the file as a buffer.
|
|
* @memberof InputFile
|
|
* @returns {Buffer}
|
|
*/
|
|
getContentsAsBuffer() {
|
|
throw new Error("Not Implemented");
|
|
}
|
|
|
|
/**
|
|
* @summary Returns the name of the package or `null` if the file is not in a
|
|
* package.
|
|
* @memberof InputFile
|
|
* @returns {String}
|
|
*/
|
|
getPackageName() {
|
|
throw new Error("Not Implemented");
|
|
}
|
|
|
|
/**
|
|
* @summary Returns the relative path of file to the package or app root
|
|
* directory. The returned path always uses forward slashes.
|
|
* @memberof InputFile
|
|
* @returns {String}
|
|
*/
|
|
getPathInPackage() {
|
|
throw new Error("Not Implemented");
|
|
}
|
|
|
|
/**
|
|
* @summary Returns a hash string for the file that can be used to implement
|
|
* caching.
|
|
* @memberof InputFile
|
|
* @returns {String}
|
|
*/
|
|
getSourceHash() {
|
|
throw new Error("Not Implemented");
|
|
}
|
|
|
|
/**
|
|
* @summary Returns the architecture that is targeted while processing this
|
|
* file.
|
|
* @memberof InputFile
|
|
* @returns {String}
|
|
*/
|
|
getArch() {
|
|
throw new Error("Not Implemented");
|
|
}
|
|
|
|
/**
|
|
* @summary Returns the full contents of the file as a string.
|
|
* @memberof InputFile
|
|
* @returns {String}
|
|
*/
|
|
getContentsAsString() {
|
|
var self = this;
|
|
return self.getContentsAsBuffer().toString('utf8');
|
|
}
|
|
|
|
/**
|
|
* @summary Returns the filename of the file.
|
|
* @memberof InputFile
|
|
* @returns {String}
|
|
*/
|
|
getBasename() {
|
|
var self = this;
|
|
return files.pathBasename(self.getPathInPackage());
|
|
}
|
|
|
|
/**
|
|
* @summary Returns the directory path relative to the package or app root.
|
|
* The returned path always uses forward slashes.
|
|
* @memberof InputFile
|
|
* @returns {String}
|
|
*/
|
|
getDirname() {
|
|
var self = this;
|
|
return files.pathDirname(self.getPathInPackage());
|
|
}
|
|
|
|
/**
|
|
* @summary Returns an object of file options such as those passed as the
|
|
* third argument to api.addFiles.
|
|
* @memberof InputFile
|
|
* @returns {Object}
|
|
*/
|
|
getFileOptions() {
|
|
throw new Error("Not Implemented");
|
|
}
|
|
|
|
/**
|
|
* @summary Call this method to raise a compilation or linting error for the
|
|
* file.
|
|
* @param {Object} options
|
|
* @param {String} options.message The error message to display.
|
|
* @param {String} [options.sourcePath] The path to display in the error message.
|
|
* @param {Integer} options.line The line number to display in the error message.
|
|
* @param {String} options.func The function name to display in the error message.
|
|
* @memberof InputFile
|
|
*/
|
|
error(options) {
|
|
var self = this;
|
|
var path = self.getPathInPackage();
|
|
var packageName = self.getPackageName();
|
|
if (packageName) {
|
|
path = "packages/" + packageName + "/" + path;
|
|
}
|
|
|
|
self._reportError(options.message || ("error building " + path), {
|
|
file: options.sourcePath || path,
|
|
line: options.line ? options.line : undefined,
|
|
column: options.column ? options.column : undefined,
|
|
func: options.func ? options.func : undefined
|
|
});
|
|
}
|
|
|
|
// Default implementation. May be overridden by subclasses.
|
|
_reportError(message, info) {
|
|
buildmessage.error(message, info);
|
|
}
|
|
}
|