Files
meteor/tools/tests/compiler-plugins.js
Paulo Mogollón 2ae2690f3a Convert tools/utils/archinfo.js to TypeScript. (#10624)
* Updated code to use modern JS
* Added types
* Stopped using 2 underscore functions (1 remaining)
2019-07-21 12:01:24 -04:00

563 lines
18 KiB
JavaScript

var _ = require('underscore');
var selftest = require('../tool-testing/selftest.js');
var files = require('../fs/files');
import { getUrl } from '../utils/http-helpers.js';
import { sleepMs } from '../utils/utils.js';
import { host } from '../utils/archinfo';
const osArch = host();
var Sandbox = selftest.Sandbox;
var MONGO_LISTENING =
{ stdout: " [initandlisten] waiting for connections on port" };
function startRun(sandbox) {
var run = sandbox.run();
run.match("myapp");
run.matchBeforeExit("Started proxy");
run.tellMongo(MONGO_LISTENING);
run.matchBeforeExit("Started MongoDB");
run.waitSecs(15);
return run;
};
// Tests the actual cache logic used by coffeescript.
selftest.define("compiler plugin caching - coffee", () => {
var s = new Sandbox({ fakeMongo: true });
s.createApp("myapp", "caching-coffee");
s.cd("myapp");
// Ask them to print out when they build a file (instead of using it from the
// cache) as well as when they load cache from disk.
s.set('METEOR_COFFEESCRIPT_CACHE_DEBUG', 't');
// Enforcing the order of builds is just too tricky if we let the legacy
// build race with the os.* build.
s.set("METEOR_DISALLOW_DELAYED_LEGACY_BUILD", "true");
var run = startRun(s);
let nextRunOrdinal = 1;
function matchRun(files, arch) {
let text = "CACHE(coffeescript): Ran (#" +
nextRunOrdinal++ + ") on: " +
JSON.stringify(files);
if (arch) {
text += " " + JSON.stringify([arch]);
}
run.match(text);
}
// First program built (server or web.browser) compiles everything.
matchRun([
'/f1.coffee',
'/f2.coffee',
'/f3.coffee',
'/packages/local-pack/p.coffee'
], "web.browser");
matchRun([
'/f1.coffee',
'/f2.coffee',
'/f3.coffee',
'/packages/local-pack/p.coffee'
], "web.browser.legacy");
matchRun([
'/f1.coffee',
'/f2.coffee',
'/f3.coffee',
'/packages/local-pack/p.coffee'
], osArch);
// App prints this:
run.match("Coffeescript X is 2 Y is 1 FromPackage is 4");
run.match("App running at");
s.write("f2.coffee", "share.Y = 'Y is 3'\n");
// Only recompiles f2.
matchRun(["/f2.coffee"], "web.browser");
matchRun(["/f2.coffee"], "web.browser.legacy");
matchRun(["/f2.coffee"], osArch);
// Program prints this:
run.match("Coffeescript X is 2 Y is 3 FromPackage is 4");
run.match("Meteor server restarted");
// Force a rebuild of the local package without actually changing the
// coffeescript file in it. This should not require us to coffee.compile
// anything (for either program).
s.append("packages/local-pack/package.js", "\n// foo\n");
matchRun([], "web.browser");
matchRun([], "web.browser.legacy");
matchRun([], osArch);
run.match("Coffeescript X is 2 Y is 3 FromPackage is 4");
run.match("Meteor server restarted");
// But writing to the actual source file in the local package should
// recompile.
s.write("packages/local-pack/p.coffee", "FromPackage = 'FromPackage is 5'");
matchRun(["/packages/local-pack/p.coffee"], "web.browser");
matchRun(["/packages/local-pack/p.coffee"], "web.browser.legacy");
matchRun(["/packages/local-pack/p.coffee"], osArch);
run.match("Coffeescript X is 2 Y is 3 FromPackage is 5");
run.match("Meteor server restarted");
// We never should have loaded cache from disk, since we only made
// each compiler once and there were no cache files at this point.
run.forbid('CACHE(coffeescript): Loaded');
// Kill the run. Change one coffee file and re-run.
run.stop();
s.write("f2.coffee", "share.Y = 'Y is edited'\n");
run = startRun(s);
// This time there's a cache to load!
run.match('CACHE(coffeescript): Loaded /packages/local-pack/p.coffee');
run.match('CACHE(coffeescript): Loaded /f1.coffee');
run.match('CACHE(coffeescript): Loaded /f3.coffee');
// And we only need to re-compiler the changed file, even though we restarted.
nextRunOrdinal = 1;
matchRun(["/f2.coffee"], "web.browser");
matchRun(["/f2.coffee"], "web.browser.legacy");
matchRun(["/f2.coffee"], osArch);
run.match('Coffeescript X is 2 Y is edited FromPackage is 5');
run.stop();
});
// Tests the actual cache logic used by less and stylus.
['less', 'stylus'].forEach((packageName) => {
const extension = packageName === 'stylus' ? 'styl' : packageName;
const hasCompileOneFileLaterSupport = packageName === "less";
selftest.define("compiler plugin caching - " + packageName, () => {
var s = new Sandbox({ fakeMongo: true });
s.createApp("myapp", "caching-" + packageName);
s.cd("myapp");
// Ask them to print out when they build a file (instead of using it from
// the cache) as well as when they load cache from disk.
s.set(`METEOR_${ packageName.toUpperCase() }_CACHE_DEBUG`, "t");
// Enforcing the order of builds is just too tricky if we let the legacy
// build race with the "Client modified - refreshing" messages.
s.set("METEOR_DISALLOW_DELAYED_LEGACY_BUILD", "true");
var run = startRun(s);
const cacheMatch = selftest.markStack((message, arch) => {
run.match(`CACHE(${
packageName
}): ${
message
}${
arch ? " " + JSON.stringify([arch]) : ""
}`);
run.waitSecs(30);
});
let nextRunOrdinal = 1;
function matchRun(files, arch) {
cacheMatch(
"Ran (#" + nextRunOrdinal++ + ") on: " +
JSON.stringify(files) +
((arch && packageName !== "stylus")
? " " + JSON.stringify([arch]) : "")
);
}
// First program built (web.browser) compiles everything.
matchRun([
// Plugins with a compileOneFileLater method can avoid compiling
// lazy files in /imports or /node_modules until they are actually
// needed, but older plugins still eagerly compile those files just
// in case they might be imported by a JS module.
...(hasCompileOneFileLaterSupport ? []
: ["/imports/dotdot." + extension]),
"/subdir/nested-root." + extension,
"/top." + extension
], "web.browser");
matchRun([
...(hasCompileOneFileLaterSupport ? []
: ["/imports/dotdot." + extension]),
"/subdir/nested-root." + extension,
"/top." + extension
], "web.browser.legacy");
// There is no render execution in the server program, because it has
// archMatching:'web'. We'll see this more clearly when the next call later
// is "#2" --- we didn't miss a call!
// App prints this:
run.waitSecs(15);
run.match("Hello world");
// Check that the CSS is what we expect.
var checkCSS = selftest.markStack((borderStyleMap) => {
var builtBrowserProgramDir = files.pathJoin(
s.cwd, '.meteor', 'local', 'build', 'programs', 'web.browser');
var cssFile = _.find(
files.readdir(
files.pathJoin(s.cwd, '.meteor/local/build/programs/web.browser')),
path => path.match(/\.css$/)
);
selftest.expectTrue(cssFile);
var actual = s.read(
files.pathJoin('.meteor/local/build/programs/web.browser', cssFile));
actual = actual.replace(/\s+/g, ''); // simplify whitespace
var expected = _.map(borderStyleMap, (style, className) => {
return '.' + className + "{border-style:" + style + ";}";
}).join('');
selftest.expectEqual(actual, expected);
});
var expectedBorderStyles = {
el0: "dashed", el1: "dotted", el2: "solid", el3: "groove", el4: "ridge"};
checkCSS(expectedBorderStyles);
// Force a rebuild of the local package without actually changing the
// preprocessor file in it. This should not require us to render anything.
s.append("packages/local-pack/package.js", "\n// foo\n");
matchRun([], "web.browser");
matchRun([], "web.browser.legacy");
run.waitSecs(15);
run.match("Hello world");
function setVariable(variableName, value) {
switch (packageName) {
case 'less':
return `@${ variableName }: ${ value };\n`;
case 'stylus':
return `$${ variableName } = ${ value }\n`;
}
}
function importLine(fileWithoutExtension) {
switch (packageName) {
case 'less':
return `@import "${ fileWithoutExtension }.less";\n`;
case 'stylus':
return `@import "${ fileWithoutExtension }.styl"\n`;
}
}
// Writing to a single file only re-renders the root that depends on it.
s.write('packages/local-pack/p.' + extension,
setVariable('el4-style', 'inset'));
expectedBorderStyles.el4 = 'inset';
matchRun([`/top.${ extension }`], "web.browser");
matchRun([`/top.${ extension }`], "web.browser.legacy");
run.match("Client modified -- refreshing");
checkCSS(expectedBorderStyles);
// This works for changing a root too.
s.write('subdir/nested-root.' + extension,
'.el0 { border-style: double; }\n');
expectedBorderStyles.el0 = 'double';
matchRun([`/subdir/nested-root.${ extension }`], "web.browser");
matchRun([`/subdir/nested-root.${ extension }`], "web.browser.legacy");
run.match("Client modified -- refreshing");
checkCSS(expectedBorderStyles);
// Adding a new root works too.
s.write('yet-another-root.' + extension,
'.el6 { border-style: solid; }\n');
expectedBorderStyles.el6 = 'solid';
matchRun([`/yet-another-root.${ extension }`], "web.browser");
matchRun([`/yet-another-root.${ extension }`], "web.browser.legacy");
run.match("Client modified -- refreshing");
checkCSS(expectedBorderStyles);
// We never should have loaded cache from disk, since we only made
// each compiler once and there were no cache files at this point.
run.forbid('CACHE(${ packageName }): Loaded');
// Kill the run. Change one file and re-run.
run.stop();
s.write('packages/local-pack/p.' + extension,
setVariable('el4-style', 'double'));
expectedBorderStyles.el4 = 'double';
run = startRun(s);
// This time there's a cache to load! Note that for
// MultiFileCachingCompiler we load all the cache entries, even for the
// not-up-to-date file 'top', because we only key off of filename, not off
// of cache key.
cacheMatch('Loaded {}/subdir/nested-root.' + extension);
cacheMatch('Loaded {}/top.' + extension);
cacheMatch('Loaded {}/yet-another-root.' + extension);
nextRunOrdinal = 1;
matchRun([`/top.${ extension }`], "web.browser");
matchRun([`/top.${ extension }`], "web.browser.legacy");
run.waitSecs(15);
run.match('Hello world');
checkCSS(expectedBorderStyles);
s.write('bad-import.' + extension, importLine('/foo/bad'));
run.match('Errors prevented startup');
switch (packageName) {
case 'less':
run.match('bad-import.less:1: Unknown import: /foo/bad.less');
break;
case 'stylus':
run.match('bad-import.styl: Stylus compiler error: bad-import.styl:1');
run.match('failed to locate @import file /foo/bad.styl');
break;
}
run.match('Waiting for file change');
run.stop();
});
});
// Tests that rebuilding a compiler plugin re-instantiates the source processor,
// but other changes don't.
selftest.define("compiler plugin caching - local plugin", function () {
var s = new Sandbox({ fakeMongo: true });
s.createApp("myapp", "local-compiler-plugin");
s.cd("myapp");
var run = startRun(s);
// The compiler gets used the first time...
run.match("PrintmeCompiler invocation 1");
// ... and the program runs the generated code.
run.match("PMC: Print out bar");
run.match("PMC: Print out foo");
s.write("quux.printme", "And print out quux");
// PrintmeCompiler gets reused.
run.match("PrintmeCompiler invocation 2");
// And the right output prints out
run.match("PMC: Print out bar");
run.match("PMC: Print out foo");
run.match("PMC: And print out quux");
// Restart meteor; see that the disk cache gets used.
run.stop();
run = startRun(s);
// Disk cache gets us up to 3.
run.match("PrintmeCompiler invocation 3");
// And the right output prints out
run.match("PMC: Print out bar");
run.match("PMC: Print out foo");
run.match("PMC: And print out quux");
// Edit the compiler itself.
s.write('packages/local-plugin/plugin.js',
s.read('packages/local-plugin/plugin.js').replace(/PMC/, 'pmc'));
// New PrintmeCompiler object, and empty disk cache dir.
run.match("PrintmeCompiler invocation 1");
// And the right output prints out (lower case now)
run.match("pmc: Print out bar");
run.match("pmc: Print out foo");
run.match("pmc: And print out quux");
run.stop();
});
// Test error on duplicate compiler plugins.
selftest.define("compiler plugins - duplicate extension", () => {
const s = new Sandbox({ fakeMongo: true });
s.createApp("myapp", "duplicate-compiler-extensions");
s.cd("myapp");
let run = startRun(s);
run.match('Errors prevented startup');
run.match('conflict: two packages');
run.match('trying to handle *.myext');
// Fix it by changing one extension.
s.write('packages/local-plugin/plugin.js',
s.read('packages/local-plugin/plugin.js').replace('myext', 'xext'));
run.match('Modified -- restarting');
run.waitSecs(30);
run.stop();
});
// Test error when a source file no longer has an active plugin.
selftest.define("compiler plugins - inactive source", () => {
const s = new Sandbox({ fakeMongo: true });
// This app depends on the published package 'glasser:uses-sourcish', and
// contains a local package 'local-plugin'.
//
// glasser:uses-sourcish depends on local-plugin and contains a file
// 'foo.sourcish'. When glasser:uses-sourcish@0.0.1 was published, a local
// copy of 'local-plugin' had a plugin which called registerCompiler for the
// extension '*.sourcish', and so 'foo.sourcish' is in the published isopack
// as a source file. However, the copy of 'local-plugin' currently in the test
// app contains no plugins. So we hit this weird error.
s.createApp('myapp', 'uses-published-package-with-inactive-source');
s.cd('myapp');
const run = s.run();
run.match('myapp');
run.matchBeforeExit('Started proxy');
run.match('Errors prevented startup');
run.match('no plugin found for foo.sourcish in glasser:use-sourcish');
run.match('none is now');
run.stop();
});
// Test error when the registerCompiler callback throws.
selftest.define("compiler plugins - compiler throws", () => {
const s = new Sandbox({ fakeMongo: true });
s.createApp('myapp', 'compiler-plugin-throws-on-instantiate');
s.cd('myapp');
const run = s.run('add', 'local-plugin');
run.matchErr('Errors while adding packages');
run.matchErr(
'While running registerCompiler callback in package local-plugin');
// XXX This is wrong! The path on disk is packages/local-plugin/plugin.js, but
// at some point we switched to the servePath which is based on the *plugin*'s
// "package" name.
run.matchErr(
/packages\/compilePrintme_plugin\.js:\d+:\d+: Error in my registerCompiler callback!/
);
run.expectExit(1);
});
function checkModernAndLegacyUrls(path, test) {
if (! path.startsWith("/")) {
path = "/" + path;
}
test(getUrl("http://localhost:3000" + path));
// Asset URLs are no longer prefixed with /__browser.legacy because the
// developer has full control over the path where an asset is served, so
// there's not much value in serving a legacy version of every asset.
// test(getUrl("http://localhost:3000/__browser.legacy" + path));
}
// Test that compiler plugins can add static assets. Also tests `filenames`
// option to registerCompiler.
selftest.define("compiler plugins - compiler addAsset", () => {
const s = new Sandbox({ fakeMongo: true });
s.createApp('myapp', 'compiler-plugin-add-asset');
s.cd('myapp');
const run = startRun(s);
// Test server-side asset.
run.match("extension is null"); // test getExtension -> null
run.match("Asset says Print out foo");
// Test client-side asset.
checkModernAndLegacyUrls("/foo.printme", body => {
selftest.expectEqual(body, "Print out foo\n");
});
run.stop();
});
// Test that a package can have a single file that is both source code and an
// asset
selftest.define("compiler plugins - addAssets", () => {
const s = new Sandbox({ fakeMongo: true });
s.createApp('myapp', 'compiler-plugin-asset-and-source');
s.cd('myapp');
const run = startRun(s);
// Test server-side asset.
run.match("Printing out my own source code!");
// Test client-side asset.
checkModernAndLegacyUrls(
"/packages/asset-and-source/asset-and-source.js",
body => {
selftest.expectTrue(
body.indexOf("Printing out my own source code!") !== -1
);
}
);
// Test that deprecated API still works (added in 1.2.1 in response to people
// having trouble upgrading to 1.2)
s.write("packages/asset-and-source/package.js", `Package.describe({
name: 'asset-and-source',
version: '0.0.1'
});
Package.onUse(function(api) {
api.addFiles('asset-and-source.js');
api.addFiles('asset-and-source.js',
['client', 'server'], { isAsset: true });
});
`);
// Test server-side asset.
run.match("Printing out my own source code!");
// Test client-side asset.
checkModernAndLegacyUrls(
"/packages/asset-and-source/asset-and-source.js",
body => {
selftest.expectTrue(
body.indexOf('Printing out my own source code!') !== -1
);
}
);
// Test error messages for malformed package files
s.write("packages/asset-and-source/package.js", `Package.describe({
name: 'asset-and-source',
version: '0.0.1'
});
Package.onUse(function(api) {
api.addFiles('asset-and-source.js');
api.addAssets('asset-and-source.js', ['client', 'server']);
api.addFiles('asset-and-source.js');
});
`);
run.match(/Duplicate source file/);
s.write("packages/asset-and-source/package.js", `Package.describe({
name: 'asset-and-source',
version: '0.0.1'
});
Package.onUse(function(api) {
api.addFiles('asset-and-source.js');
api.addAssets('asset-and-source.js', ['client', 'server']);
api.addAssets('asset-and-source.js', ['client', 'server']);
});
`);
run.match(/Duplicate asset file/);
s.write("packages/asset-and-source/package.js", `Package.describe({
name: 'asset-and-source',
version: '0.0.1'
});
Package.onUse(function(api) {
api.addAssets('asset-and-source.js');
});
`);
run.match(/requires a second argument/);
run.stop();
});