var main = require('./main.js'); var _ = require('underscore'); var files = require('../fs/files'); var deploy = require('../meteor-services/deploy.js'); var buildmessage = require('../utils/buildmessage.js'); var auth = require('../meteor-services/auth.js'); var config = require('../meteor-services/config.js'); var runLog = require('../runners/run-log.js'); var utils = require('../utils/utils.js'); var httpHelpers = require('../utils/http-helpers.js'); var archinfo = require('../utils/archinfo'); var catalog = require('../packaging/catalog/catalog.js'); var stats = require('../meteor-services/stats.js'); var Console = require('../console/console.js').Console; const { blue, green, purple, red, yellow } = require('../console/console.js').colors; var projectContextModule = require('../project-context.js'); var release = require('../packaging/release.js'); const { Profile } = require("../tool-env/profile"); import { ensureDevBundleDependencies } from '../cordova/index.js'; import { CordovaRunner } from '../cordova/runner.js'; import { iOSRunTarget, AndroidRunTarget } from '../cordova/run-targets.js'; import { EXAMPLE_REPOSITORIES } from './example-repositories.js'; // The architecture used by Meteor Software's hosted servers; it's the // architecture used by 'meteor deploy'. var DEPLOY_ARCH = 'os.linux.x86_64'; // The default port that the development server listens on. var DEFAULT_PORT = '3000'; // __dirname - the location of the current executing file var __dirnameConverted = files.convertToStandardPath(__dirname); // Given a site name passed on the command line (eg, 'mysite'), return // a fully-qualified hostname ('mysite.meteor.com'). // // This is fairly simple for now. It appends 'meteor.com' if the name // doesn't contain a dot, and it deletes any trailing dots (the // technically legal hostname 'mysite.com.' is canonicalized to // 'mysite.com'). // // In the future, you should be able to make this default to some // other domain you control, rather than 'meteor.com'. var qualifySitename = function (site) { if (site.indexOf(".") === -1) { site = site + ".meteor.com"; } while (site.length && site[site.length - 1] === ".") { site = site.substring(0, site.length - 1); } return site; }; // Display a message showing valid Meteor architectures. var showInvalidArchMsg = function (arch) { Console.info("Invalid architecture: " + arch); Console.info("The following are valid Meteor architectures:"); Object.keys(archinfo.VALID_ARCHITECTURES).forEach(function (va) { Console.info( Console.command(va), Console.options({ indent: 2 })); }); }; // Utility functions to parse options in run/build/test-packages commands export function parseServerOptionsForRunCommand(options, runTargets) { const parsedServerUrl = parsePortOption(options.port); const mobileServerOption = options['mobile-server']; let parsedMobileServerUrl; if (mobileServerOption) { parsedMobileServerUrl = parseMobileServerOption(mobileServerOption); } else { const isRunOnDeviceRequested = _.any(runTargets, runTarget => runTarget.isDevice); parsedMobileServerUrl = detectMobileServerUrl(parsedServerUrl, isRunOnDeviceRequested); } const parsedCordovaServerPort = parseCordovaServerPortOption(options); return { parsedServerUrl, parsedMobileServerUrl, parsedCordovaServerPort }; } function parsePortOption(portOption) { let parsedServerUrl = utils.parseUrl(portOption); if (!parsedServerUrl.port) { Console.error("--port must include a port."); throw new main.ExitWithCode(1); } return parsedServerUrl; } function parseMobileServerOption(mobileServerOption, optionName = 'mobile-server') { let parsedMobileServerUrl = utils.parseUrl( mobileServerOption, { protocol: 'http' }); if (!parsedMobileServerUrl.hostname) { Console.error(`--${optionName} must include a hostname.`); throw new main.ExitWithCode(1); } return parsedMobileServerUrl; } function parseCordovaServerPortOption(options = {}) { const cordovaServerPortOption = options['cordova-server-port']; return cordovaServerPortOption ? parseInt(cordovaServerPortOption, 10) : null; } function detectMobileServerUrl(parsedServerUrl, isRunOnDeviceRequested) { // Always try to use an auto-detected IP first try { const myIp = utils.ipAddress(); return { protocol: 'http', hostname: myIp, port: parsedServerUrl.port }; } catch (error) { // Unless we are being asked to run on a device, use localhost as fallback if (isRunOnDeviceRequested) { Console.error( `Error detecting IP address for mobile app to connect to: ${error.message} Please specify the address that the mobile app should connect to with --mobile-server.`); throw new main.ExitWithCode(1); } else { return { protocol: 'http', hostname: 'localhost', port: parsedServerUrl.port }; } } } export function parseRunTargets(targets) { return targets.map((target) => { const targetParts = target.split('-'); const platform = targetParts[0]; const isDevice = targetParts[1] === 'device'; if (platform == 'ios') { return new iOSRunTarget(isDevice); } else if (platform == 'android') { return new AndroidRunTarget(isDevice); } else { Console.error(`Unknown run target: ${target}`); throw new main.ExitWithCode(1); } }); }; const excludableWebArchs = ['web.browser', 'web.browser.legacy', 'web.cordova']; function filterWebArchs(webArchs, excludeArchsOption) { if (excludeArchsOption) { const excludeArchs = excludeArchsOption.trim().split(/\s*,\s*/) .filter(arch => excludableWebArchs.includes(arch)); webArchs = webArchs.filter(arch => !excludeArchs.includes(arch)); } return webArchs; } /////////////////////////////////////////////////////////////////////////////// // options that act like commands /////////////////////////////////////////////////////////////////////////////// // Prints the Meteor architecture name of this host main.registerCommand({ name: '--arch', requiresRelease: false, pretty: false, catalogRefresh: new catalog.Refresh.Never() }, function (options) { Console.rawInfo(archinfo.host() + "\n"); }); // Prints the current release in use. Note that if there is not // actually a specific release, we print to stderr and exit non-zero, // while if there is a release we print to stdout and exit zero // (making this useful to scripts). // XXX: What does this mean in our new release-free world? main.registerCommand({ name: '--version', requiresRelease: false, pretty: false, catalogRefresh: new catalog.Refresh.Never() }, function (options) { if (release.current === null) { if (! options.appDir) { throw new Error("missing release, but not in an app?"); } Console.error( "This project was created with a checkout of Meteor, rather than an " + "official release, and doesn't have a release number associated with " + "it. You can set its release with " + Console.command("'meteor update'") + "."); return 1; } if (release.current.isCheckout()) { var gitLog = utils.runGitInCheckout( 'log', '--format=%h%d', '-n 1').trim(); Console.error("Unreleased, running from a checkout at " + gitLog); return 1; } Console.info(release.current.getDisplayName()); }); // Internal use only. For automated testing. main.registerCommand({ name: '--long-version', requiresRelease: false, pretty: false, catalogRefresh: new catalog.Refresh.Never() }, function (options) { if (files.inCheckout()) { Console.error("checkout"); return 1; } else if (release.current === null) { // .meteor/release says "none" but not in a checkout. Console.error("none"); return 1; } else { Console.rawInfo(release.current.name + "\n"); Console.rawInfo(files.getToolsVersion() + "\n"); return 0; } }); // Internal use only. For automated testing. main.registerCommand({ name: '--requires-release', requiresRelease: true, pretty: false, catalogRefresh: new catalog.Refresh.Never() }, function (options) { return 0; }); /////////////////////////////////////////////////////////////////////////////// // run /////////////////////////////////////////////////////////////////////////////// const inspectOptions = { "inspect": { type: String, implicitValue: "9229" }, "inspect-brk": { type: String, implicitValue: "9229" }, }; function normalizeInspectOptions(options) { const result = Object.create(null); if (_.has(options, "debug-port")) { console.log( "The --debug-port option is deprecated; " + "please use --inspect-brk= instead." ); if (! _.has(options, "inspect-brk")) { options["inspect-brk"] = options["debug-port"]; } delete options["debug-port"]; } if (_.has(options, "inspect-brk")) { result.inspect = { port: options["inspect-brk"], "break": true, }; if (_.has(options, "inspect")) { console.log( "Both --inspect and --inspect-brk provided; " + "ignoring --inspect." ); delete options.inspect; } } else if (_.has(options, "inspect")) { result.inspect = { port: options.inspect, "break": false, }; } return result; } var runCommandOptions = { requiresApp: true, maxArgs: Infinity, options: { port: { type: String, short: "p", default: DEFAULT_PORT }, 'mobile-server': { type: String }, 'cordova-server-port': { type: String }, 'app-port': { type: String }, 'debug-port': { type: String }, ...inspectOptions, 'no-release-check': { type: Boolean }, production: { type: Boolean }, 'raw-logs': { type: Boolean }, settings: { type: String, short: "s" }, verbose: { type: Boolean, short: "v" }, // With --once, meteor does not re-run the project if it crashes // and does not monitor for file changes. Intentionally // undocumented: intended for automated testing (eg, cli-test.sh), // not end-user use. #Once once: { type: Boolean }, // Don't run linter on rebuilds 'no-lint': { type: Boolean }, // Allow the version solver to make breaking changes to the versions // of top-level dependencies. 'allow-incompatible-update': { type: Boolean }, 'extra-packages': { type: String }, 'exclude-archs': { type: String } }, catalogRefresh: new catalog.Refresh.Never() }; main.registerCommand(Object.assign( { name: 'run' }, runCommandOptions ), doRunCommand); function doRunCommand(options) { Console.setVerbose(!!options.verbose); // Additional args are interpreted as run targets const runTargets = parseRunTargets(options.args); const { parsedServerUrl, parsedMobileServerUrl, parsedCordovaServerPort } = parseServerOptionsForRunCommand(options, runTargets); var includePackages = []; if (options['extra-packages']) { includePackages = options['extra-packages'].trim().split(/\s*,\s*/); } var projectContext = new projectContextModule.ProjectContext({ projectDir: options.appDir, allowIncompatibleUpdate: options['allow-incompatible-update'], lintAppAndLocalPackages: !options['no-lint'], includePackages: includePackages, }); main.captureAndExit("=> Errors while initializing project:", function () { // We're just reading metadata here --- we'll wait to do the full build // preparation until after we've started listening on the proxy, etc. projectContext.readProjectMetadata(); }); if (release.explicit) { if (release.current.name !== projectContext.releaseFile.fullReleaseName) { console.log("=> Using %s as requested (overriding %s)", release.current.getDisplayName(), projectContext.releaseFile.displayReleaseName); console.log(); } } let appHost, appPort; if (options['app-port']) { var appPortMatch = options['app-port'].match(/^(?:(.+):)?([0-9]+)?$/); if (!appPortMatch) { Console.error( "run: --app-port must be a number or be of the form 'host:port' ", "where port is a number. Try", Console.command("'meteor help run'") + " for help."); return 1; } appHost = appPortMatch[1] || null; // It's legit to specify `--app-port host:` and still let the port be // randomized. appPort = appPortMatch[2] ? parseInt(appPortMatch[2]) : null; } if (options.production) { Console.warn( "Warning: The --production flag should only be used to simulate production " + "bundling for testing purposes. Use meteor build to create a bundle for " + "production deployment. See: https://guide.meteor.com/deployment.html" ); } if (options['raw-logs']) { runLog.setRawLogs(true); } let webArchs = projectContext.platformList.getWebArchs(); if (! _.isEmpty(runTargets) || options['mobile-server']) { if (webArchs.indexOf("web.cordova") < 0) { webArchs.push("web.cordova"); } } webArchs = filterWebArchs(webArchs, options['exclude-archs']); const buildMode = options.production ? 'production' : 'development' let cordovaRunner; if (!_.isEmpty(runTargets)) { function prepareCordovaProject() { import { CordovaProject } from '../cordova/project.js'; main.captureAndExit('', 'preparing Cordova project', () => { const cordovaProject = new CordovaProject(projectContext, { settingsFile: options.settings, mobileServerUrl: utils.formatUrl(parsedMobileServerUrl), cordovaServerPort: parsedCordovaServerPort, buildMode }); if (buildmessage.jobHasMessages()) return; cordovaRunner = new CordovaRunner(cordovaProject, runTargets); cordovaRunner.checkPlatformsForRunTargets(); }); } ensureDevBundleDependencies(); prepareCordovaProject(); } var runAll = require('../runners/run-all.js'); return runAll.run({ projectContext: projectContext, proxyPort: parsedServerUrl.port, proxyHost: parsedServerUrl.hostname, appPort: appPort, appHost: appHost, ...normalizeInspectOptions(options), settingsFile: options.settings, buildOptions: { minifyMode: options.production ? 'production' : 'development', buildMode, webArchs: webArchs }, rootUrl: process.env.ROOT_URL, mongoUrl: process.env.MONGO_URL, oplogUrl: process.env.MONGO_OPLOG_URL, mobileServerUrl: utils.formatUrl(parsedMobileServerUrl), cordovaServerPort: parsedCordovaServerPort, once: options.once, noReleaseCheck: options['no-release-check'] || process.env.METEOR_NO_RELEASE_CHECK, cordovaRunner: cordovaRunner }); } /////////////////////////////////////////////////////////////////////////////// // debug /////////////////////////////////////////////////////////////////////////////// main.registerCommand(Object.assign( { name: 'debug' }, runCommandOptions ), function (options) { options["inspect-brk"] = options["inspect-brk"] || "9229"; return doRunCommand(options); }); /////////////////////////////////////////////////////////////////////////////// // shell /////////////////////////////////////////////////////////////////////////////// main.registerCommand({ name: 'shell', requiresRelease: false, requiresApp: true, pretty: false, catalogRefresh: new catalog.Refresh.Never() }, function (options) { if (!options.appDir) { Console.error( "The " + Console.command("'meteor shell'") + " command must be run", "in a Meteor app directory." ); } else { var projectContext = new projectContextModule.ProjectContext({ projectDir: options.appDir }); // Convert to OS path here because shell/server.js doesn't know how to // convert paths, since it exists in the app and in the tool. require('../shell-client').connect( files.convertToOSPath(projectContext.getMeteorShellDirectory()) ); throw new main.WaitForExit; } }); /////////////////////////////////////////////////////////////////////////////// // create /////////////////////////////////////////////////////////////////////////////// const DEFAULT_SKELETON = "react"; export const AVAILABLE_SKELETONS = [ "apollo", "bare", "blaze", "full", "minimal", DEFAULT_SKELETON, "typescript", "vue", 'vue-2', "svelte", "tailwind", "chakra-ui", "solid", ]; main.registerCommand({ name: 'create', maxArgs: 1, options: { list: { type: Boolean }, example: { type: String }, package: { type: Boolean }, bare: { type: Boolean }, minimal: { type: Boolean }, full: { type: Boolean }, blaze: { type: Boolean }, react: { type: Boolean }, vue: { type: Boolean }, 'vue-2': { type: Boolean }, typescript: { type: Boolean }, apollo: { type: Boolean }, svelte: { type: Boolean }, tailwind: { type: Boolean }, 'chakra-ui': { type: Boolean }, solid: { type: Boolean }, prototype: { type: Boolean } }, catalogRefresh: new catalog.Refresh.Never() }, function (options) { // Creating a package is much easier than creating an app, so if that's what // we are doing, do that first. (For example, we don't springboard to the // latest release to create a package if we are inside an app) if (options.package) { var packageName = options.args[0]; if (options.prototype) { Console.error( `The ${Console.command('--prototype')} option is no longer supported for packages.` ); Console.error(); throw new main.ShowUsage; } if (options.list || options.example) { Console.error("No package examples exist at this time."); Console.error(); throw new main.ShowUsage; } if (!packageName) { Console.error("Please specify the name of the package."); throw new main.ShowUsage; } utils.validatePackageNameOrExit( packageName, {detailedColonExplanation: true}); // When we create a package, avoid introducing a colon into the file system // by naming the directory after the package name without the prefix. var fsName = packageName; if (packageName.indexOf(":") !== -1) { var split = packageName.split(":"); if (split.length > 2) { // It may seem like this check should be inside package version parser's // validatePackageName, but we decided to name test packages like this: // local-test:prefix:name, so we have to support building packages // with at least two colons. Therefore we will at least try to // discourage people from putting a ton of colons in their package names // here. Console.error(packageName + ": Package names may not have more than one colon."); return 1; } fsName = split[1]; } var packageDir; if (options.appDir) { packageDir = files.pathResolve(options.appDir, 'packages', fsName); } else { packageDir = files.pathResolve(fsName); } var inYourApp = options.appDir ? " in your app" : ""; if (files.exists(packageDir)) { Console.error(packageName + ": Already exists" + inYourApp); return 1; } var transform = function (x) { var xn = x.replace(/~name~/g, packageName).replace(/~fs-name~/g, fsName); // If we are running from checkout, comment out the line sourcing packages // from a release, with the latest release filled in (in case they do want // to publish later). If we are NOT running from checkout, fill it out // with the current release. var relString; if (release.current.isCheckout()) { xn = xn.replace(/~cc~/g, "//"); var rel = catalog.official.getDefaultReleaseVersion(); // the no-release case should never happen except in tests. relString = rel ? rel.version : "no-release"; } else { xn = xn.replace(/~cc~/g, ""); relString = release.current.getDisplayName({noPrefix: true}); } // If we are not in checkout, write the current release here. return xn.replace(/~release~/g, relString); }; try { files.cp_r(files.pathJoin(__dirnameConverted, '..', 'static-assets', 'skel-pack'), packageDir, { transformFilename: function (f) { return transform(f); }, transformContents: function (contents, f) { if ((/(\.html|\.[jt]sx?|\.css)/).test(f)) { return Buffer.from(transform(contents.toString())); } else { return contents; } }, ignore: [/^local$/], preserveSymlinks: true, }); } catch (err) { Console.error("Could not create package: " + err.message); return 1; } var displayPackageDir = files.convertToOSPath(files.pathRelative(files.cwd(), packageDir)); // Since the directory can't have colons, the directory name will often not // match the name of the package exactly, therefore we should tell people // where it was created. Console.info( packageName + ": created in", Console.path(displayPackageDir) ); return 0; } // Suppose you have an app A, and from some directory inside that // app, you run 'meteor create /my/new/app'. The new app should use // the latest available Meteor release, not the release that A // uses. So if we were run from inside an app directory, and the // user didn't force a release with --release, we need to // springboard to the correct release and tools version. // // (In particular, it's not sufficient to create the new app with // this version of the tools, and then stamp on the correct release // at the end.) if (! release.current.isCheckout() && !release.forced) { if (release.current.name !== release.latestKnown()) { throw new main.SpringboardToLatestRelease; } } if (options.list) { Console.info("Available examples:"); _.each(EXAMPLE_REPOSITORIES, function (repoInfo, name) { const branchInfo = repoInfo.branch ? `/tree/${repoInfo.branch}` : ''; Console.info( Console.command(`${name}: ${repoInfo.repo}${branchInfo}`), Console.options({ indent: 2 })); }); Console.info(); Console.info("To create an example, simply", Console.command("git clone"), "the relevant repository and branch (run", Console.command("'meteor create --example '"), " to see the full command)."); return 0; }; if (options.example) { const repoInfo = EXAMPLE_REPOSITORIES[options.example]; if (!repoInfo) { Console.error(`${options.example}: no such example.`); Console.error( "List available applications with", Console.command("'meteor create --list'") + "."); return 1; } const branchOption = repoInfo.branch ? ` -b ${repoInfo.branch}` : ''; const path = options.args.length === 1 ? ` ${options.args[0]}` : ''; Console.info(`To create the ${options.example} example, please run:`); Console.info( Console.command(`git clone ${repoInfo.repo}${branchOption}${path}`), Console.options({ indent: 2 })); return 0; } var appPathAsEntered; if (options.args.length === 1) { appPathAsEntered = options.args[0]; } else { throw new main.ShowUsage; } var appPath = files.pathResolve(appPathAsEntered); if (files.findAppDir(appPath)) { Console.error( "You can't create a Meteor project inside another Meteor project."); return 1; } var appName; if (appPathAsEntered === "." || appPathAsEntered === "./") { // If trying to create in current directory appName = files.pathBasename(files.cwd()); } else { appName = files.pathBasename(appPath); } var transform = function (x) { return x.replace(/~name~/g, appName); }; // These file extensions are usually metadata, not app code var nonCodeFileExts = ['.txt', '.md', '.json', '.sh']; var destinationHasCodeFiles = false; // If the directory doesn't exist, it clearly doesn't have any source code // inside itself if (files.exists(appPath)) { destinationHasCodeFiles = _.any(files.readdir(appPath), function thisPathCountsAsAFile(filePath) { // We don't mind if there are hidden files or directories (this includes // .git) and we don't need to check for .meteor here because the command // will fail earlier var isHidden = /^\./.test(filePath); if (isHidden) { // Not code return false; } // We do mind if there are non-hidden directories, because we don't want // to recursively check everything to do some crazy heuristic to see if // we should try to create an app. var stats = files.stat(files.pathJoin(appPath, filePath)); if (stats.isDirectory()) { // Could contain code return true; } // Check against our file extension white list var ext = files.pathExtname(filePath); if (ext == '' || nonCodeFileExts.includes(ext)) { return false; } // Everything not matched above is considered to be possible source code return true; }); } var toIgnore = [/^local$/, /^\.id$/]; if (destinationHasCodeFiles) { // If there is already source code in the directory, don't copy our // skeleton app code over it. Just create the .meteor folder and metadata toIgnore.push(/(\.html|\.js|\.css)/); } const skeletonExplicitOption = AVAILABLE_SKELETONS.find(skeleton => !!options[skeleton]); const skeleton = skeletonExplicitOption || DEFAULT_SKELETON; files.cp_r(files.pathJoin(__dirnameConverted, '..', 'static-assets', `skel-${skeleton}`), appPath, { transformFilename: function (f) { return transform(f); }, transformContents: function (contents, f) { // check if this app is just for prototyping if it is then we need to add autopublish and insecure in the packages file if ((/packages/).test(f)) { const prototypePackages = () => 'autopublish # Publish all data to the clients (for prototyping)\n' + 'insecure # Allow all DB writes from clients (for prototyping)'; // XXX: if there is the need to add more options maybe we should have a better abstraction for this if-else if (options.prototype) { return Buffer.from(contents.toString().replace(/~prototype~/g, prototypePackages())) } else { return Buffer.from(contents.toString().replace(/~prototype~/g, '')) } } if ((/(\.html|\.[jt]sx?|\.css)/).test(f)) { return Buffer.from(transform(contents.toString())); } else { return contents; } }, ignore: toIgnore, preserveSymlinks: true, }); // We are actually working with a new meteor project at this point, so // set up its context. var projectContext = new projectContextModule.ProjectContext({ projectDir: appPath, // Write .meteor/versions even if --release is specified. alwaysWritePackageMap: true, // examples come with a .meteor/versions file, but we shouldn't take it // too seriously allowIncompatibleUpdate: true }); main.captureAndExit("=> Errors while creating your project", function () { projectContext.readProjectMetadata(); if (buildmessage.jobHasMessages()) { return; } projectContext.releaseFile.write( release.current.isCheckout() ? "none" : release.current.name); if (buildmessage.jobHasMessages()) { return; } // Also, write package version constraints from the current release // If we are on a checkout, we don't need to do this as running from // checkout still pins all package versions and if the user updates // to a real release, the packages file will subsequently get updated if (!release.current.isCheckout()) { projectContext.projectConstraintsFile .updateReleaseConstraints(release.current._manifest); } // Any upgrader that is in this version of Meteor doesn't need to be run on // this project. var upgraders = require('../upgraders.js'); projectContext.finishedUpgraders.appendUpgraders(upgraders.allUpgraders()); projectContext.prepareProjectForBuild(); }); // No need to display the PackageMapDelta here, since it would include all of // the packages (or maybe an unpredictable subset based on what happens to be // in the template's versions file). // Since some of the project skeletons include npm `devDependencies`, we need // to make sure they're included when running `npm install`. require("./default-npm-deps.js").install( appPath, { includeDevDependencies: true } ); var appNameToDisplay = appPathAsEntered === "." ? "current directory" : `'${appPathAsEntered}'`; var message = `Created a new Meteor app in ${appNameToDisplay}`; message += "."; Console.info(message + "\n"); // Print a nice message telling people we created their new app, and what to // do next. Console.info("To run your new app:"); function cmd(text) { Console.info(Console.command(text), Console.options({ indent: 2 })); } if (appPathAsEntered !== ".") { // Wrap the app path in quotes if it contains spaces const appPathWithQuotesIfSpaces = appPathAsEntered.indexOf(' ') === -1 ? appPathAsEntered : `'${appPathAsEntered}'`; // Don't tell people to 'cd .' cmd("cd " + appPathWithQuotesIfSpaces); } cmd("meteor"); Console.info(""); Console.info("If you are new to Meteor, try some of the learning resources here:"); Console.info( Console.url("https://www.meteor.com/tutorials"), Console.options({ indent: 2 })); Console.info(""); Console.info("When you’re ready to deploy and host your new Meteor application, check out Cloud:"); Console.info( Console.url("https://www.meteor.com/cloud"), Console.options({ indent: 2 })); if (!!skeletonExplicitOption) { // Notify people about the skeleton options Console.info([ "", "To start with a different app template, try one of the following:", "", ].join("\n")); cmd("meteor create --bare # to create an empty app"); cmd("meteor create --minimal # to create an app with as few Meteor packages as possible"); cmd("meteor create --full # to create a more complete scaffolded app"); cmd("meteor create --react # to create a basic React-based app"); cmd("meteor create --vue # to create a basic Vue3-based app"); cmd("meteor create --vue-2 # to create a basic Vue2-based app"); cmd("meteor create --apollo # to create a basic Apollo + React app"); cmd("meteor create --svelte # to create a basic Svelte app"); cmd("meteor create --typescript # to create an app using TypeScript and React"); cmd("meteor create --blaze # to create an app using Blaze"); cmd("meteor create --tailwind # to create an app using React and Tailwind"); cmd("meteor create --chakra-ui # to create an app Chakra UI and React"); cmd("meteor create --solid # to create a basic Solid app"); } Console.info(""); }); /////////////////////////////////////////////////////////////////////////////// // build /////////////////////////////////////////////////////////////////////////////// var buildCommands = { minArgs: 1, maxArgs: 1, requiresApp: true, options: { debug: { type: Boolean }, packageType: { type: String }, directory: { type: Boolean }, architecture: { type: String }, "server-only": { type: Boolean }, 'mobile-settings': { type: String }, server: { type: String }, "cordova-server-port": { type: String }, // Indicates whether these build is running headless, e.g. in a // continuous integration building environment, where visual niceties // like progress bars and spinners are unimportant. headless: { type: Boolean }, verbose: { type: Boolean, short: "v" }, 'allow-incompatible-update': { type: Boolean }, platforms: { type: String } }, catalogRefresh: new catalog.Refresh.Never() }; main.registerCommand({ name: "build", ...buildCommands, }, async function (options) { return Profile.run( "meteor build", () => Promise.await(buildCommand(options)) ); }); // Deprecated -- identical functionality to 'build' with one exception: it // doesn't output a directory with all builds but rather only one tarball with // server/client programs. // XXX COMPAT WITH 0.9.1.1 main.registerCommand({ name: "bundle", hidden: true, ...buildCommands, }, async function (options) { Console.error( "This command has been deprecated in favor of " + Console.command("'meteor build'") + ", which allows you to " + "build for multiple platforms and outputs a directory instead of " + "a single tarball. See " + Console.command("'meteor help build'") + " " + "for more information."); Console.error(); return Profile.run( "meteor bundle", () => Promise.await(buildCommand({ ...options, _bundleOnly: true, })) ); }); var buildCommand = function (options) { Console.setVerbose(!!options.verbose); if (options.headless) { // There's no point in spinning the spinner when we're running // automated builds. Console.setHeadless(true); } // XXX output, to stderr, the name of the file written to (for human // comfort, especially since we might change the name) // XXX name the root directory in the bundle based on the basename // of the file, not a constant 'bundle' (a bit obnoxious for // machines, but worth it for humans) // Error handling for options.architecture. See archinfo for more // information on what the architectures are, what they mean, et cetera. if (options.architecture && !_.has(archinfo.VALID_ARCHITECTURES, options.architecture)) { showInvalidArchMsg(options.architecture); return 1; } var bundleArch = options.architecture || archinfo.host(); var projectContext = new projectContextModule.ProjectContext({ projectDir: options.appDir, serverArchitectures: _.uniq([bundleArch, archinfo.host()]), allowIncompatibleUpdate: options['allow-incompatible-update'] }); main.captureAndExit("=> Errors while initializing project:", function () { // TODO Fix the nested Profile.run warning here, without interfering // with METEOR_PROFILE output for other commands, like `meteor run`. projectContext.prepareProjectForBuild(); }); projectContext.packageMapDelta.displayOnConsole(); // _bundleOnly implies serverOnly const serverOnly = options._bundleOnly || !!options['server-only']; // options['mobile-settings'] is used to set the initial value of // `Meteor.settings` on mobile apps. Pass it on to options.settings, // which is used in this command. if (options['mobile-settings']) { options.settings = options['mobile-settings']; } const appName = files.pathBasename(options.appDir); let parsedCordovaServerPort; let selectedPlatforms = null; if (options.platforms) { const platformsArray = options.platforms.split(","); platformsArray.forEach(plat => { if (![...excludableWebArchs, 'android', 'ios'].includes(plat)) { throw new Error(`Not allowed platform on '--platforms' flag: ${plat}`) } }) selectedPlatforms = platformsArray; } let cordovaPlatforms; let parsedMobileServerUrl; if (!serverOnly) { cordovaPlatforms = projectContext.platformList.getCordovaPlatforms(); if (selectedPlatforms) { cordovaPlatforms = _.intersection(selectedPlatforms, cordovaPlatforms) } if (process.platform !== 'darwin' && cordovaPlatforms.includes('ios')) { cordovaPlatforms = _.without(cordovaPlatforms, 'ios'); Console.warn("Currently, it is only possible to build iOS apps \ on an OS X system."); } if (!_.isEmpty(cordovaPlatforms)) { const mobileServerOption = options.server; if (!mobileServerOption) { // For Cordova builds, require '--server'. // XXX better error message? Console.error( "Supply the server hostname and port in the --server option " + "for mobile app builds."); return 1; } parsedMobileServerUrl = parseMobileServerOption(mobileServerOption, 'server'); parsedCordovaServerPort = parseCordovaServerPortOption(options); } } else { cordovaPlatforms = []; } // If we specified some platforms, we need to build what was specified. // For example, if we want to build only android, there is no need to build // web.browser. let webArchs; if (selectedPlatforms) { const filteredArchs = projectContext.platformList .getWebArchs() .filter(arch => selectedPlatforms.includes(arch)); if ( !_.isEmpty(cordovaPlatforms) && !filteredArchs.includes('web.cordova') ) { filteredArchs.push('web.cordova'); } webArchs = filteredArchs.length ? filteredArchs : undefined; } var buildDir = projectContext.getProjectLocalDirectory('build_tar'); var outputPath = files.pathResolve(options.args[0]); // get absolute path // Warn if people try to build inside the app directory. var relative = files.pathRelative(options.appDir, outputPath); // We would like the output path to be outside the app directory, which // means the first step to getting there is going up a level. if (relative.substr(0, 2) !== '..') { Console.warn(); Console.labelWarn(`The output directory is under your source tree. Your generated files may get interpreted as source code! Consider building into a different directory instead ${Console.command("meteor build ../output")}`, Console.options({ indent: 2 })); Console.warn(); } var bundlePath = options.directory ? (options._bundleOnly ? outputPath : files.pathJoin(outputPath, 'bundle')) : files.pathJoin(buildDir, 'bundle'); stats.recordPackages({ what: "sdk.bundle", projectContext: projectContext }); var bundler = require('../isobuild/bundler.js'); var bundleResult = bundler.bundle({ projectContext: projectContext, outputPath: bundlePath, buildOptions: { minifyMode: options.debug ? 'development' : 'production', // XXX is this a good idea, or should linux be the default since // that's where most people are deploying // default? i guess the problem with using DEPLOY_ARCH as default // is then 'meteor bundle' with no args fails if you have any local // packages with binary npm dependencies serverArch: bundleArch, buildMode: options.debug ? 'development' : 'production', webArchs, }, }); if (bundleResult.errors) { Console.error("Errors prevented bundling:"); Console.error(bundleResult.errors.formatMessages()); return 1; } if (!options._bundleOnly) { files.mkdir_p(outputPath); } if (!options.directory) { main.captureAndExit('', 'creating server tarball', () => { try { var outputTar = options._bundleOnly ? outputPath : files.pathJoin(outputPath, appName + '.tar.gz'); files.createTarball(files.pathJoin(buildDir, 'bundle'), outputTar); } catch (err) { buildmessage.exception(err); files.rm_recursive(buildDir); } }); } if (!_.isEmpty(cordovaPlatforms)) { let cordovaProject; main.captureAndExit('', () => { import { pluginVersionsFromStarManifest, displayNameForPlatform, } from '../cordova/index.js'; ensureDevBundleDependencies(); buildmessage.enterJob({ title: "preparing Cordova project" }, () => { import { CordovaProject } from '../cordova/project.js'; cordovaProject = new CordovaProject(projectContext, { settingsFile: options.settings, mobileServerUrl: utils.formatUrl(parsedMobileServerUrl), cordovaServerPort: parsedCordovaServerPort }); if (buildmessage.jobHasMessages()) return; const pluginVersions = pluginVersionsFromStarManifest( bundleResult.starManifest); cordovaProject.prepareFromAppBundle(bundlePath, pluginVersions); }); for (platform of cordovaPlatforms) { buildmessage.enterJob( { title: `building Cordova app for \ ${displayNameForPlatform(platform)}` }, () => { let buildOptions = { release: !options.debug }; const buildPath = files.pathJoin( projectContext.getProjectLocalDirectory('cordova-build'), 'platforms', platform); const platformOutputPath = files.pathJoin(outputPath, platform); // Prepare the project once again to ensure that it is up to date // with current build options. For example, --server=example.com // is utilized in the Cordova builder to write boilerplate HTML and // various config.xml settings (e.g. access policies) if (platform === 'ios') { cordovaProject.prepareForPlatform(platform, buildOptions); } else if (platform === 'android') { cordovaProject.buildForPlatform(platform, {...buildOptions, argv: ["--packageType", options.packageType || "bundle"]}); } // Once prepared, copy the bundle to the final location. files.cp_r(buildPath, files.pathJoin(platformOutputPath, 'project')); // Make some platform-specific adjustments to the resulting build. if (platform === 'ios') { files.writeFile( files.pathJoin(platformOutputPath, 'README'), `This is an auto-generated XCode project for your iOS application. Instructions for publishing your iOS app to App Store can be found at: https://guide.meteor.com/cordova.html#submitting-ios `, "utf8"); } else if (platform === 'android') { const packageType = options.packageType || "bundle" const packageExtension = packageType === 'bundle' ? 'aab' : 'apk'; const packageName = packageType === 'bundle' ? `app-release` : `app-release-unsigned`; const apkPath = files.pathJoin(buildPath, `app/build/outputs/${packageType}/${options.debug ? 'debug' : 'release'}`, options.debug ? `app-debug.${packageExtension}` : `${packageName}.${packageExtension}`); console.log(apkPath) if (files.exists(apkPath)) { files.copyFile(apkPath, files.pathJoin(platformOutputPath, options.debug ? `app-debug.${packageExtension}` : `${packageName}.${packageExtension}`)); } files.writeFile( files.pathJoin(platformOutputPath, 'README'), `This is an auto-generated Gradle project for your Android application. Instructions for publishing your Android app to Play Store can be found at: https://guide.meteor.com/cordova.html#submitting-android `, "utf8"); } }); } }); } files.rm_recursive(buildDir); }; /////////////////////////////////////////////////////////////////////////////// // lint /////////////////////////////////////////////////////////////////////////////// main.registerCommand({ name: 'lint', maxArgs: 0, requiresAppOrPackage: true, options: { 'allow-incompatible-update': { type: Boolean }, // This option has never done anything, but we are keeping it for // backwards compatibility since it existed for 7 years before adding // the correctly named option 'allow-incompatible-updates': { type: Boolean } }, catalogRefresh: new catalog.Refresh.Never() }, function (options) { const {packageDir, appDir} = options; let projectContext = null; // if the goal is to lint the package, don't include the whole app if (packageDir) { // similar to `meteor publish`, create a fake project const tempProjectDir = files.mkdtemp('meteor-package-build'); projectContext = new projectContextModule.ProjectContext({ projectDir: tempProjectDir, explicitlyAddedLocalPackageDirs: [packageDir], packageMapFilename: files.pathJoin(packageDir, '.versions'), alwaysWritePackageMap: true, forceIncludeCordovaUnibuild: true, allowIncompatibleUpdate: options['allow-incompatible-update'], lintPackageWithSourceRoot: packageDir }); main.captureAndExit("=> Errors while setting up package:", () => // Read metadata and initialize catalog. projectContext.initializeCatalog() ); const versionRecord = projectContext.localCatalog.getVersionBySourceRoot(packageDir); if (! versionRecord) { throw Error("explicitly added local package dir missing?"); } const packageName = versionRecord.packageName; const constraint = utils.parsePackageConstraint(packageName); projectContext.projectConstraintsFile.removeAllPackages(); projectContext.projectConstraintsFile.addConstraints([constraint]); } // linting the app if (! projectContext && appDir) { projectContext = new projectContextModule.ProjectContext({ projectDir: appDir, serverArchitectures: [archinfo.host()], allowIncompatibleUpdate: options['allow-incompatible-update'], lintAppAndLocalPackages: true }); } main.captureAndExit("=> Errors prevented the build:", () => { projectContext.prepareProjectForBuild(); }); const bundler = require('../isobuild/bundler.js'); const bundle = bundler.bundle({ projectContext: projectContext, outputPath: null, buildOptions: { minifyMode: 'development' } }); const displayName = options.packageDir ? 'package' : 'app'; if (bundle.errors) { Console.error( `=> Errors building your ${displayName}:\n\n${bundle.errors.formatMessages()}` ); throw new main.ExitWithCode(2); } if (bundle.warnings && bundle.warnings.hasMessages()) { Console.warn(bundle.warnings.formatMessages()); return 1; } return 0; }); /////////////////////////////////////////////////////////////////////////////// // mongo /////////////////////////////////////////////////////////////////////////////// main.registerCommand({ name: 'mongo', maxArgs: 1, options: { url: { type: Boolean, short: 'U' }, verbose: { type: Boolean, short: 'V' }, }, requiresApp: function (options) { return options.args.length === 0; }, pretty: false, catalogRefresh: new catalog.Refresh.Never() }, function (options) { var mongoUrl; var usedMeteorAccount = false; if (options.args.length === 0) { // localhost mode var findMongoPort = require('../runners/run-mongo.js').findMongoPort; var mongoPort = findMongoPort(files.pathJoin(options.appDir, ".meteor", "local", "db")); // XXX detect the case where Meteor is running, but MONGO_URL was // specified? if (! mongoPort) { Console.info("mongo: Meteor isn't running a local MongoDB server."); Console.info(); Console.info(`\ This command only works while Meteor is running your application locally. \ Start your application first with 'meteor' and then run this command in a new \ terminal. This error will also occur if you asked Meteor to use a different \ MongoDB server with $MONGO_URL when you ran your application.`); Console.info(); Console.info(`\ If you're trying to connect to the database of an app you deployed with \ ${Console.command("'meteor deploy'")}, specify your site's name as an argument \ to this command.`); return 1; } mongoUrl = "mongodb://127.0.0.1:" + mongoPort + "/meteor"; } else { // remote mode var site = qualifySitename(options.args[0]); mongoUrl = deploy.temporaryMongoUrl(site); usedMeteorAccount = true; if (!mongoUrl) { // temporaryMongoUrl() will have printed an error message return 1; } } if (options.url) { console.log(`${yellow`$`} ${ purple`mongosh` } ${ blue(mongoUrl) }`); } else { if (usedMeteorAccount) { auth.maybePrintRegistrationLink(); } process.stdin.pause(); var runMongo = require('../runners/run-mongo.js'); runMongo.runMongoShell(mongoUrl, (err) => { console.log(red`Some error occured while trying to run mongosh.`); console.log(yellow`Check bellow for some more info:`); console.log(` Since version v5.0.5 the mongo shell has been superseded by the mongosh below there is the url to use with mongosh ${yellow`$`} ${ purple`mongosh` } ${ blue(mongoUrl) } `) if (err.code === 'ENOENT') { console.log(red`The 'mongosh' command line tool was not found in your PATH.`); console.log(`Check https://www.mongodb.com/docs/mongodb-shell/`); process.exit(2); return; } if (options.verbose) { console.log("here is a more verbose error message:"); console.log(yellow`=====================================`); console.log(err); console.log(yellow`=====================================`); } process.exit(1); }); throw new main.WaitForExit; } }); /////////////////////////////////////////////////////////////////////////////// // reset /////////////////////////////////////////////////////////////////////////////// main.registerCommand({ name: 'reset', // Doesn't actually take an argument, but we want to print an custom // error message if they try to pass one. maxArgs: 1, requiresApp: true, catalogRefresh: new catalog.Refresh.Never() }, function (options) { if (options.args.length !== 0) { Console.error("meteor reset only affects the locally stored database."); Console.error(); Console.error("To reset a deployed application use"); Console.error( Console.command("meteor deploy --delete appname"), Console.options({ indent: 2 })); Console.error("followed by"); Console.error( Console.command("meteor deploy appname"), Console.options({ indent: 2 })); return 1; } if (process.env.MONGO_URL) { Console.info("As a precaution, meteor reset only clears the local database that is " + "provided by meteor run for development. The database specified with " + "MONGO_URL will NOT be reset."); } // XXX detect the case where Meteor is running the app, but // MONGO_URL was set, so we don't see a Mongo process var findMongoPort = require('../runners/run-mongo.js').findMongoPort; var isRunning = !! findMongoPort(files.pathJoin(options.appDir, ".meteor", "local", "db")); if (isRunning) { Console.error("reset: Meteor is running."); Console.error(); Console.error( "This command does not work while Meteor is running your application.", "Exit the running Meteor development server."); return 1; } return files.rm_recursive_async( files.pathJoin(options.appDir, '.meteor', 'local') ).then(() => { Console.info("Project reset."); }); }); /////////////////////////////////////////////////////////////////////////////// // deploy /////////////////////////////////////////////////////////////////////////////// main.registerCommand({ name: 'deploy', minArgs: 0, maxArgs: 1, options: { 'delete': { type: Boolean, short: 'D' }, debug: { type: Boolean }, settings: { type: String, short: 's' }, // No longer supported, but we still parse it out so that we can // print a custom error message. password: { type: String }, // Override architecture to deploy whatever stuff we have locally, even if // it contains binary packages that should be incompatible. A hack to allow // people to deploy from checkout or do other weird shit. We are not // responsible for the consequences. 'override-architecture-with-local' : { type: Boolean }, 'allow-incompatible-update': { type: Boolean }, 'deploy-polling-timeout': { type: Number }, 'no-wait': { type: Boolean }, // Useful to cache the build between deploys, in some cases people deploy // the same build to different hostnames 'cache-build': { type: Boolean }, // Useful when you want to build first to have a cache-build and then deploy // many apps 'build-only': { type: Boolean }, free: { type: Boolean }, plan: { type: String }, 'container-size': { type: String }, 'deploy-token': { type: String }, mongo: { type: Boolean }, owner: { type: String } }, allowUnrecognizedOptions: true, requiresApp: function (options) { return ! options.delete; }, catalogRefresh: new catalog.Refresh.Never() }, async function (...args) { return Profile.run( "meteor deploy", () => Promise.await(deployCommand(...args)) ); }); function deployCommand(options, { rawOptions }) { const site = options.args[0]; if (options.delete) { return deploy.deleteApp(site); } if (options.password) { Console.error( "Setting passwords on apps is no longer supported. Now there are " + "user accounts and your apps are associated with your account so " + "that only you (and people you designate) can access them. See the " + Console.command("'meteor authorized'") + " command."); return 1; } const loggedIn = auth.isLoggedIn(); if (! loggedIn && !options["deploy-token"]) { Console.error( "You must be logged in to deploy, just enter your email address."); Console.error(); if (! auth.registerOrLogIn()) { return 1; } } // Override architecture iff applicable. let buildArch = DEPLOY_ARCH; if (options['override-architecture-with-local']) { Console.warn(); Console.labelWarn( "OVERRIDING DEPLOY ARCHITECTURE WITH LOCAL ARCHITECTURE.", "If your app contains binary code, it may break in unexpected " + "and terrible ways."); buildArch = archinfo.host(); } const projectContext = new projectContextModule.ProjectContext({ projectDir: options.appDir, serverArchitectures: _.uniq([buildArch, archinfo.host()]), allowIncompatibleUpdate: options['allow-incompatible-update'] }); main.captureAndExit("=> Errors while initializing project:", function () { // TODO Fix nested Profile.run warning here, too. projectContext.prepareProjectForBuild(); }); projectContext.packageMapDelta.displayOnConsole(); const buildOptions = { minifyMode: options.debug ? 'development' : 'production', buildMode: options.debug ? 'development' : 'production', serverArch: buildArch }; let deployPollingTimeoutMs = null; if (options['deploy-polling-timeout']) { deployPollingTimeoutMs = options['deploy-polling-timeout']; } let plan = null; if (options.plan) { plan = options.plan; } let containerSize = null; if (options['container-size']) { containerSize = options['container-size']; } const isCacheBuildEnabled = !!options['cache-build']; const isBuildOnly = !!options['build-only']; const waitForDeploy = !options['no-wait']; const deployResult = deploy.bundleAndDeploy({ projectContext, site, settingsFile: options.settings, free: options.free, deployToken: options['deploy-token'], owner: options.owner, mongo: options.mongo, buildOptions: buildOptions, plan, containerSize, rawOptions, deployPollingTimeoutMs, waitForDeploy, isCacheBuildEnabled, isBuildOnly, }); if (deployResult === 0) { auth.maybePrintRegistrationLink({ leadingNewline: true, // If the user was already logged in at the beginning of the // deploy, then they've already been prompted to set a password // at least once before, so we use a slightly different message. firstTime: ! loggedIn }); } return deployResult; } /////////////////////////////////////////////////////////////////////////////// // authorized /////////////////////////////////////////////////////////////////////////////// main.registerCommand({ name: 'authorized', minArgs: 1, maxArgs: 1, options: { add: { type: String, short: "a" }, transfer: { type: String, short: "t" }, remove: { type: String, short: "r" }, list: { type: Boolean } }, pretty: function (options) { // pretty if we're mutating; plain if we're listing (which is more likely to // be used by scripts) return options.add || options.remove || options.transfer; }, catalogRefresh: new catalog.Refresh.Never() }, function (options) { if (Object.keys(_.pick(options, 'add', 'remove', 'transfer', 'list')).length > 1) { Console.error( "Sorry, you can only perform one authorization operation at a time."); return 1; } auth.pollForRegistrationCompletion(); var site = qualifySitename(options.args[0]); if (! auth.isLoggedIn()) { Console.error( "You must be logged in for that. Try " + Console.command("'meteor login'")); return 1; } if (options.add) { return deploy.changeAuthorized(site, "add", options.add); } else if (options.remove) { return deploy.changeAuthorized(site, "remove", options.remove); } else if (options.transfer) { return deploy.changeAuthorized(site, "transfer", options.transfer); } else { return deploy.listAuthorized(site); } }); /////////////////////////////////////////////////////////////////////////////// // test and test-packages /////////////////////////////////////////////////////////////////////////////// testCommandOptions = { maxArgs: Infinity, catalogRefresh: new catalog.Refresh.Never(), options: { port: { type: String, short: "p", default: DEFAULT_PORT }, 'mobile-server': { type: String }, 'cordova-server-port': { type: String }, 'debug-port': { type: String }, ...inspectOptions, 'no-release-check': { type: Boolean }, deploy: { type: String }, production: { type: Boolean }, settings: { type: String, short: 's' }, // Indicates whether these self-tests are running headless, e.g. in a // continuous integration testing environment, where visual niceties // like progress bars and spinners are unimportant. headless: { type: Boolean }, verbose: { type: Boolean, short: "v" }, 'raw-logs': { type: Boolean }, // Undocumented. See #Once once: { type: Boolean }, // Undocumented. To ensure that QA covers both // PollingObserveDriver and OplogObserveDriver, this option // disables oplog for tests. (It still creates a replset, it just // doesn't do oplog tailing.) 'disable-oplog': { type: Boolean }, // Undocumented flag to use a different test driver. 'driver-package': { type: String }, // Sets the path of where the temp app should be created 'test-app-path': { type: String }, // Undocumented, runs tests under selenium 'selenium': { type: Boolean }, 'selenium-browser': { type: String }, // Undocumented. Usually we just show a banner saying 'Tests' instead of // the ugly path to the temporary test directory, but if you actually want // to see it you can ask for it. 'show-test-app-path': { type: Boolean }, // hard-coded options with all known Cordova platforms ios: { type: Boolean }, 'ios-device': { type: Boolean }, android: { type: Boolean }, 'android-device': { type: Boolean }, // This could theoretically be useful/necessary in conjunction with // --test-app-path. 'allow-incompatible-update': { type: Boolean }, // Don't print linting messages for tested packages 'no-lint': { type: Boolean }, // allow excluding packages when testing all packages. // should be a comma-separated list of package names. 'exclude': { type: String }, // one of the following must be true 'test': { type: Boolean, 'default': false }, 'test-packages': { type: Boolean, 'default': false }, // For 'test-packages': Run in "full app" mode 'full-app': { type: Boolean, 'default': false }, 'extra-packages': { type: String }, 'exclude-archs': { type: String } } }; main.registerCommand(Object.assign({ name: 'test', requiresApp: true }, testCommandOptions), function (options) { options['test'] = true; return doTestCommand(options); }); main.registerCommand(Object.assign( { name: 'test-packages' }, testCommandOptions ), function (options) { options['test-packages'] = true; return doTestCommand(options); }); function doTestCommand(options) { // This "metadata" is accessed in a few places. Using a global // variable here was more expedient than navigating the many layers // of abstraction across the the build process. // // As long as the Meteor CLI runs a single command as part of each // process, this should be safe. global.testCommandMetadata = {}; Console.setVerbose(!!options.verbose); if (options.headless) { Console.setHeadless(true); } const runTargets = parseRunTargets(_.intersection( Object.keys(options), ['ios', 'ios-device', 'android', 'android-device'])); const { parsedServerUrl, parsedMobileServerUrl, parsedCordovaServerPort } = parseServerOptionsForRunCommand(options, runTargets); // Make a temporary app dir (based on the test runner app). This will be // cleaned up on process exit. Using a temporary app dir means that we can // run multiple "test-packages" commands in parallel without them stomping // on each other. let testRunnerAppDir; const testAppPath = options['test-app-path']; if (testAppPath) { const absTestAppPath = files.pathResolve(testAppPath); try { if (files.mkdir_p(absTestAppPath, 0o700)) { testRunnerAppDir = absTestAppPath; } else { Console.error( 'The specified --test-app-path directory could not be used, as ' + `"${testAppPath}" already exists and it is not a directory.` ); return 1; } } catch (error) { Console.error( 'Unable to create the specified --test-app-path directory of ' + `"${testAppPath}".` ); throw error; } } if (!testRunnerAppDir) { testRunnerAppDir = files.mkdtemp('meteor-test-run'); } // Download packages for our architecture, and for the deploy server's // architecture if we're deploying. var serverArchitectures = [archinfo.host()]; if (options.deploy && DEPLOY_ARCH !== archinfo.host()) { serverArchitectures.push(DEPLOY_ARCH); } if (options['raw-logs']) { runLog.setRawLogs(true); } var includePackages = []; if (options['extra-packages']) { includePackages = options['extra-packages'].trim().split(/\s*,\s*/); } if (options['driver-package']) { includePackages.push( global.testCommandMetadata.driverPackage = options['driver-package'].trim() ); } else if (options["test-packages"]) { includePackages.push( global.testCommandMetadata.driverPackage = "test-in-browser" ); } var projectContextOptions = { serverArchitectures: serverArchitectures, allowIncompatibleUpdate: options['allow-incompatible-update'], lintAppAndLocalPackages: !options['no-lint'], includePackages: includePackages }; var projectContext; if (options["test-packages"]) { projectContextOptions.projectDir = testRunnerAppDir; projectContextOptions.projectDirForLocalPackages = options.appDir; try { require("./default-npm-deps.js").install(testRunnerAppDir); } catch (error) { if (error.code === 'EACCES' && options['test-app-path']) { Console.error( 'The specified --test-app-path directory of ' + `"${testRunnerAppDir}" exists, but the current user does not have ` + `read/write permission in it.` ); } throw error; } if (buildmessage.jobHasMessages()) { return; } // Find any packages mentioned by a path instead of a package name. We will // load them explicitly into the catalog. var packagesByPath = _.filter(options.args, function (p) { return p.indexOf('/') !== -1; }); // If we're currently in an app, we still want to use the real app's // packages subdirectory, not the test runner app's empty one. projectContextOptions.explicitlyAddedLocalPackageDirs = packagesByPath; // XXX Because every run uses a new app with its own IsopackCache directory, // this always does a clean build of all packages. Maybe we can speed up // repeated test-packages calls with some sort of shared or semi-shared // isopack cache that's specific to test-packages? See #3012. projectContext = new projectContextModule.ProjectContext(projectContextOptions); main.captureAndExit("=> Errors while initializing project:", function () { // We're just reading metadata here --- we'll wait to do the full build // preparation until after we've started listening on the proxy, etc. projectContext.readProjectMetadata(); }); main.captureAndExit("=> Errors while setting up tests:", function () { // Read metadata and initialize catalog. projectContext.initializeCatalog(); }); // Overwrite .meteor/release. projectContext.releaseFile.write( release.current.isCheckout() ? "none" : release.current.name); var packagesToAdd = getTestPackageNames(projectContext, options.args); // filter out excluded packages var excludedPackages = options.exclude && options.exclude.split(','); if (excludedPackages) { packagesToAdd = _.filter(packagesToAdd, function (p) { return ! _.some(excludedPackages, function (excluded) { return p.replace(/^local-test:/, '') === excluded; }); }); } // Use the driver package if running `meteor test-packages`. For // `meteor test`, the driver package is expected to already // have been added to the app. packagesToAdd.unshift(global.testCommandMetadata.driverPackage); // Also, add `autoupdate` so that you don't have to manually refresh the tests packagesToAdd.unshift("autoupdate"); var constraintsToAdd = _.map(packagesToAdd, function (p) { return utils.parsePackageConstraint(p); }); // Add the packages to our in-memory representation of .meteor/packages. (We // haven't yet resolved constraints, so this will affect constraint // resolution.) This will get written to disk once we prepareProjectForBuild, // either in the Cordova code below, right before deploying below, or in the // app runner. (Note that removeAllPackages removes any comments from // .meteor/packages, but that's OK since this isn't a real user project.) projectContext.projectConstraintsFile.removeAllPackages(); projectContext.projectConstraintsFile.addConstraints(constraintsToAdd); // Write these changes to disk now, so that if the first attempt to prepare // the project for build hits errors, we don't lose them on // projectContext.reset. projectContext.projectConstraintsFile.writeIfModified(); } else if (options["test"]) { if (!options['driver-package']) { throw new Error("You must specify a driver package with --driver-package"); } global.testCommandMetadata.driverPackage = options['driver-package']; global.testCommandMetadata.isAppTest = options['full-app']; global.testCommandMetadata.isTest = !global.testCommandMetadata.isAppTest; projectContextOptions.projectDir = options.appDir; projectContextOptions.projectLocalDir = files.pathJoin(testRunnerAppDir, '.meteor', 'local'); // Copy the existing build and isopacks to speed up the initial start function copyDirIntoTestRunnerApp(allowSymlink, ...parts) { // Depending on whether the user has run `meteor run` or other commands, they // may or may not exist yet const appDirPath = files.pathJoin(options.appDir, ...parts); const testDirPath = files.pathJoin(testRunnerAppDir, ...parts); files.mkdir_p(appDirPath); files.mkdir_p(files.pathDirname(testDirPath)); if (allowSymlink) { // Windows can create junction links without administrator // privileges since both paths refer to directories. files.symlink(appDirPath, testDirPath, "junction"); } else { files.cp_r(appDirPath, testDirPath, { preserveSymlinks: true }); } } copyDirIntoTestRunnerApp(false, '.meteor', 'local', 'build'); copyDirIntoTestRunnerApp(true, '.meteor', 'local', 'bundler-cache'); copyDirIntoTestRunnerApp(true, '.meteor', 'local', 'isopacks'); copyDirIntoTestRunnerApp(true, '.meteor', 'local', 'plugin-cache'); copyDirIntoTestRunnerApp(true, '.meteor', 'local', 'shell'); projectContext = new projectContextModule.ProjectContext(projectContextOptions); main.captureAndExit("=> Errors while setting up tests:", function () { // Read metadata and initialize catalog. projectContext.initializeCatalog(); }); } else { throw new Error("Unexpected: neither test-packages nor test"); } // The rest of the projectContext preparation process will happen inside the // runner, once the proxy is listening. The changes we made were persisted to // disk, so projectContext.reset won't make us forget anything. let cordovaRunner; if (!_.isEmpty(runTargets)) { function prepareCordovaProject() { main.captureAndExit('', 'preparing Cordova project', () => { import { CordovaProject } from '../cordova/project.js'; const cordovaProject = new CordovaProject(projectContext, { settingsFile: options.settings, mobileServerUrl: utils.formatUrl(parsedMobileServerUrl), cordovaServerPort: parsedCordovaServerPort }); if (buildmessage.jobHasMessages()) return; cordovaRunner = new CordovaRunner(cordovaProject, runTargets); projectContext.platformList.write(cordovaRunner.platformsForRunTargets); cordovaRunner.checkPlatformsForRunTargets(); }); } ensureDevBundleDependencies(); prepareCordovaProject(); } options.cordovaRunner = cordovaRunner; return runTestAppForPackages(projectContext, Object.assign( options, { mobileServerUrl: utils.formatUrl(parsedMobileServerUrl), cordovaServerPort: parsedCordovaServerPort, proxyPort: parsedServerUrl.port, proxyHost: parsedServerUrl.hostname, } )); } // Returns the "local-test:*" package names for the given package names (or for // all local packages if packageNames is empty/unspecified). var getTestPackageNames = function (projectContext, packageNames) { var packageNamesSpecifiedExplicitly = ! _.isEmpty(packageNames); if (_.isEmpty(packageNames)) { // If none specified, test all local packages. (We don't have tests for // non-local packages.) packageNames = projectContext.localCatalog.getAllPackageNames(); } var testPackages = []; main.captureAndExit("=> Errors while collecting tests:", function () { _.each(packageNames, function (p) { buildmessage.enterJob("trying to test package `" + p + "`", function () { // If it's a package name, look it up the normal way. if (p.indexOf('/') === -1) { if (p.indexOf('@') !== -1) { buildmessage.error( "You may not specify versions for local packages: " + p ); return; // recover by ignoring } // Check to see if this is a real local package, and if it is a real // local package, if it has tests. var version = projectContext.localCatalog.getLatestVersion(p); if (! version) { buildmessage.error("Not a known local package, cannot test"); } else if (version.testName) { testPackages.push(version.testName); } else if (packageNamesSpecifiedExplicitly) { // It's only an error to *ask* to test a package with no tests, not // to come across a package with no tests when you say "test all // packages". buildmessage.error("Package has no tests"); } } else { // Otherwise, it's a directory; find it by source root. version = projectContext.localCatalog.getVersionBySourceRoot( files.pathResolve(p)); if (! version) { buildmessage.error("Package not found in local catalog"); return; } if (version.testName) { testPackages.push(version.testName); } // It is not an error to mention a package by directory that is a // package but has no tests; this means you can run `meteor // test-packages $APP/packages/*` without having to worry about the // packages that don't have tests. } }); }); }); return testPackages; }; var runTestAppForPackages = function (projectContext, options) { var buildOptions = { minifyMode: options.production ? 'production' : 'development' }; buildOptions.buildMode = "test"; let webArchs = projectContext.platformList.getWebArchs(); if (options.cordovaRunner) { webArchs.push("web.cordova"); } buildOptions.webArchs = filterWebArchs(webArchs, options['exclude-archs']); if (options.deploy) { // Run the constraint solver and build local packages. main.captureAndExit("=> Errors while initializing project:", function () { projectContext.prepareProjectForBuild(); }); // No need to display the PackageMapDelta here, since it would include all // of the packages! buildOptions.serverArch = DEPLOY_ARCH; return deploy.bundleAndDeploy({ projectContext: projectContext, site: options.deploy, settingsFile: options.settings, buildOptions: buildOptions, recordPackageUsage: false }); } else { var runAll = require('../runners/run-all.js'); return runAll.run({ projectContext: projectContext, proxyPort: options.proxyPort, proxyHost: options.proxyHost, ...normalizeInspectOptions(options), disableOplog: options['disable-oplog'], settingsFile: options.settings, testMetadata: global.testCommandMetadata, banner: options['show-test-app-path'] ? null : "Tests", buildOptions: buildOptions, rootUrl: process.env.ROOT_URL, mongoUrl: process.env.MONGO_URL, oplogUrl: process.env.MONGO_OPLOG_URL, mobileServerUrl: options.mobileServerUrl, once: options.once, noReleaseCheck: options['no-release-check'] || process.env.METEOR_NO_RELEASE_CHECK, recordPackageUsage: false, selenium: options.selenium, seleniumBrowser: options['selenium-browser'], cordovaRunner: options.cordovaRunner, // On the first run, we shouldn't display the delta between "no packages // in the temp app" and "all the packages we're testing". If we make // changes and reload, though, it's fine to display them. omitPackageMapDeltaDisplayOnFirstRun: true }); } }; /////////////////////////////////////////////////////////////////////////////// // rebuild /////////////////////////////////////////////////////////////////////////////// main.registerCommand({ name: 'rebuild', maxArgs: Infinity, hidden: true, requiresApp: true, catalogRefresh: new catalog.Refresh.Never(), 'allow-incompatible-update': { type: Boolean } }, function (options) { var projectContextModule = require('../project-context.js'); var projectContext = new projectContextModule.ProjectContext({ projectDir: options.appDir, forceRebuildPackages: options.args.length ? options.args : true, allowIncompatibleUpdate: options['allow-incompatible-update'] }); main.captureAndExit("=> Errors while rebuilding packages:", function () { projectContext.prepareProjectForBuild(); }); projectContext.packageMapDelta.displayOnConsole(); Console.info("Packages rebuilt."); }); /////////////////////////////////////////////////////////////////////////////// // login /////////////////////////////////////////////////////////////////////////////// main.registerCommand({ name: 'login', options: { email: { type: Boolean } }, catalogRefresh: new catalog.Refresh.Never() }, function (options) { return auth.loginCommand(Object.assign({ overwriteExistingToken: true }, options)); }); /////////////////////////////////////////////////////////////////////////////// // logout /////////////////////////////////////////////////////////////////////////////// main.registerCommand({ name: 'logout', catalogRefresh: new catalog.Refresh.Never() }, function (options) { return auth.logoutCommand(options); }); /////////////////////////////////////////////////////////////////////////////// // whoami /////////////////////////////////////////////////////////////////////////////// main.registerCommand({ name: 'whoami', catalogRefresh: new catalog.Refresh.Never(), pretty: false }, function (options) { return auth.whoAmICommand(options); }); /////////////////////////////////////////////////////////////////////////////// // organizations /////////////////////////////////////////////////////////////////////////////// var loggedInAccountsConnectionOrPrompt = function (action) { var token = auth.getSessionToken(config.getAccountsDomain()); if (! token) { Console.error("You must be logged in to " + action + "."); auth.doUsernamePasswordLogin({ retry: true }); Console.info(); } token = auth.getSessionToken(config.getAccountsDomain()); var conn = auth.loggedInAccountsConnection(token); if (conn === null) { // Server rejected our token. Console.error("You must be logged in to " + action + "."); auth.doUsernamePasswordLogin({ retry: true }); Console.info(); token = auth.getSessionToken(config.getAccountsDomain()); conn = auth.loggedInAccountsConnection(token); } return conn; }; // List the organizations of which the current user is a member. main.registerCommand({ name: 'admin list-organizations', minArgs: 0, maxArgs: 0, pretty: false, catalogRefresh: new catalog.Refresh.Never() }, function (options) { var token = auth.getSessionToken(config.getAccountsDomain()); if (! token) { Console.error("You must be logged in to list your organizations."); auth.doUsernamePasswordLogin({ retry: true }); Console.info(); } var url = config.getAccountsApiUrl() + "/organizations"; try { var result = httpHelpers.request({ url: url, method: "GET", useSessionHeader: true, useAuthHeader: true }); var body = JSON.parse(result.body); } catch (err) { Console.error("Error listing organizations."); return 1; } if (result.response.statusCode === 401 && body && body.error === "invalid_credential") { Console.error("You must be logged in to list your organizations."); // XXX It would be nice to do a username/password prompt here like // we do for the other orgs commands. return 1; } if (result.response.statusCode !== 200 || ! body || ! body.organizations) { Console.error("Error listing organizations."); return 1; } if (body.organizations.length === 0) { Console.info("You are not a member of any organizations."); } else { Console.rawInfo(_.pluck(body.organizations, "name").join("\n") + "\n"); } return 0; }); main.registerCommand({ name: 'admin members', minArgs: 1, maxArgs: 1, options: { add: { type: String }, remove: { type: String }, list: { type: Boolean } }, pretty: function (options) { // pretty if we're mutating; plain if we're listing (which is more likely to // be used by scripts) return options.add || options.remove; }, catalogRefresh: new catalog.Refresh.Never() }, function (options) { if (options.add && options.remove) { Console.error( "Sorry, you can only add or remove one member at a time."); throw new main.ShowUsage; } var username = options.add || options.remove; var conn = loggedInAccountsConnectionOrPrompt( username ? "edit organizations" : "show an organization's members"); if (username ) { // Adding or removing members try { conn.call( options.add ? "addOrganizationMember": "removeOrganizationMember", options.args[0], username); } catch (err) { Console.error("Error " + (options.add ? "adding" : "removing") + " member: " + err.reason); return 1; } Console.info(username + " " + (options.add ? "added to" : "removed from") + " organization " + options.args[0] + "."); } else { // Showing the members of an org try { var result = conn.call("showOrganization", options.args[0]); } catch (err) { Console.error("Error showing organization: " + err.reason); return 1; } var members = _.pluck(result, "username"); Console.rawInfo(members.join("\n") + "\n"); } return 0; }); /////////////////////////////////////////////////////////////////////////////// // self-test /////////////////////////////////////////////////////////////////////////////// // XXX we should find a way to make self-test fully self-contained, so that it // ignores "packageDirs" (ie, it shouldn't fail just because you happen to be // sitting in an app with packages that don't build) main.registerCommand({ name: 'self-test', minArgs: 0, maxArgs: 1, options: { changed: { type: Boolean }, 'force-online': { type: Boolean }, slow: { type: Boolean }, galaxy: { type: Boolean }, browserstack: { type: Boolean }, phantom: { type: Boolean }, // Indicates whether these self-tests are running headless, e.g. in a // continuous integration testing environment, where visual niceties // like progress bars and spinners are unimportant. headless: { type: Boolean }, history: { type: Number }, list: { type: Boolean }, file: { type: String }, exclude: { type: String }, // Skip tests w/ this tag 'without-tag': { type: String }, // Only run tests with this tag 'with-tag': { type: String }, junit: { type: String }, retries: { type: Number, default: 2 }, // Skip tests, after filter skip: { type: Number }, // Limit tests, after filter limit: { type: Number }, // Don't run tests, just show the plan after filter, skip and limit preview: { type: Boolean }, }, hidden: true, catalogRefresh: new catalog.Refresh.Never() }, function (options) { if (! files.inCheckout()) { Console.error("self-test is only supported running from a checkout"); return 1; } var selftest = require('../tool-testing/selftest.js'); // Auto-detect whether to skip 'net' tests, unless --force-online is passed. var offline = false; if (!options['force-online']) { try { require('../utils/http-helpers.js').getUrl("http://www.google.com/"); } catch (e) { if (e instanceof files.OfflineError) { offline = true; } } } var compileRegexp = function (str) { try { return new RegExp(str); } catch (e) { if (!(e instanceof SyntaxError)) { throw e; } Console.error("Bad regular expression: " + str); return null; } }; var testRegexp = undefined; if (options.args.length) { testRegexp = compileRegexp(options.args[0]); if (! testRegexp) { return 1; } } var fileRegexp = undefined; if (options.file) { fileRegexp = compileRegexp(options.file); if (! fileRegexp) { return 1; } } var excludeRegexp = undefined; if (options.exclude) { excludeRegexp = compileRegexp(options.exclude); if (! excludeRegexp) { return 1; } } if (options.list) { selftest.listTests({ onlyChanged: options.changed, offline: offline, includeSlowTests: options.slow, galaxyOnly: options.galaxy, testRegexp: testRegexp, fileRegexp: fileRegexp, 'without-tag': options['without-tag'], 'with-tag': options['with-tag'] }); return 0; } const clients = { puppeteer: true, // Puppeteer is always enabled. phantom: options.phantom, browserstack: options.browserstack, }; if (options.headless) { // There's no point in spinning the spinner when we're running // continuous integration tests. Console.setHeadless(true); } return selftest.runTests({ // filtering options onlyChanged: options.changed, offline: offline, includeSlowTests: options.slow, galaxyOnly: options.galaxy, testRegexp: testRegexp, fileRegexp: fileRegexp, excludeRegexp: excludeRegexp, // other options retries: options.retries, historyLines: options.history, clients: clients, junit: options.junit && files.pathResolve(options.junit), 'without-tag': options['without-tag'], 'with-tag': options['with-tag'], skip: options.skip, limit: options.limit, preview: options.preview, }); }); /////////////////////////////////////////////////////////////////////////////// // list-sites /////////////////////////////////////////////////////////////////////////////// main.registerCommand({ name: 'list-sites', minArgs: 0, maxArgs: 0, pretty: false, catalogRefresh: new catalog.Refresh.Never() }, function (options) { auth.pollForRegistrationCompletion(); if (! auth.isLoggedIn()) { Console.error( "You must be logged in for that. Try " + Console.command("'meteor login'") + "."); return 1; } return deploy.listSites(); }); /////////////////////////////////////////////////////////////////////////////// // generate /////////////////////////////////////////////////////////////////////////////// /** * * @param question * @returns {function(string): Promise} */ const createPrompt = () => { const readline = require('readline') .createInterface({ input: process.stdin, output: process.stdout }); return async (question) => new Promise((resolve, reject) => { readline.question(question, (answer) => { resolve(answer); }) }) } const sanitizeBoolAnswer = (string) => { if (string === '') return true; if (string.toLowerCase() === 'y' || string.toLowerCase() === 'yes') return true; if (string.toLowerCase() === 'n' || string.toLowerCase() === 'no' ) return false; Console.error(red('You must provide a valid answer')); Console.error(yellow('it should be either (y)es or (n)o or just press enter to accept the default value')); throw new main.ExitWithCode(2); } /** * simple verification for the name * @param scaffoldName {string} */ const checkScaffoldName = (scaffoldName) => { if (scaffoldName === '') { Console.error(red('You must provide a name for your model.')); Console.error(yellow('Model names should not be empty.')); throw new main.ExitWithCode(2); } if (scaffoldName.includes('/')) { Console.error(red('You must provide a valid name for your model.')); Console.error(yellow('Model names should not contain slashes.')); throw new main.ExitWithCode(2); } const allNonWordRegex = /[^a-zA-Z0-9_-]/g; // all numbers and letters plus _ and - if (allNonWordRegex.test(scaffoldName)) { Console.error(red('You must provide a valid name for your model.')); Console.error(yellow('Model names should not contain special characters except _ and -')); throw new main.ExitWithCode(2); } } main.registerCommand({ name: 'generate', maxArgs: 1, minArgs: 0, options: { path: { type: String }, methods: { type: Boolean }, publications: { type: Boolean }, templatePath : { type: String }, replaceFn : { type: String }, }, pretty: false, catalogRefresh: new catalog.Refresh.Never() }, async function (options) { const { args, appDir } = options; const setup = async (arg0) => { if (arg0 === undefined) { const ask = createPrompt(); // the ANSI color chart is here: https://en.wikipedia.org/wiki/ANSI_escape_code#Colors const scaffoldName = await ask(`What is the name of your ${yellow`model`}? `); checkScaffoldName(scaffoldName); const areMethods = await ask(`There will be methods [${green`Y`}/${red`n`}]? press enter for ${green`yes`} `); const methods = sanitizeBoolAnswer(areMethods); const arePublications = await ask(`There will be publications [${green`Y`}/${red`n`}]? press enter for ${green`yes`} `); const publications = sanitizeBoolAnswer(arePublications); const path = await ask(`Where it will be placed? press enter for ${yellow`./imports/api/`} `); return { isWizard: true, scaffoldName, path, methods, publications, } } const { path, methods, publications } = options; return { isWizard: false, scaffoldName: arg0, path, methods, publications, } } /** * @type{string} */ const { isWizard, scaffoldName, path, methods, publications } = await setup(args[0]); checkScaffoldName(scaffoldName); // get directory where we will place our files const scaffoldPath = path ||`${ appDir }/imports/api/${ scaffoldName }`; /** * * @param appDir * @returns {string[]} */ const getFilesInDir = (appDir) => { const appPath = files.pathResolve(appDir); return files.readdir(appPath); } const getExtension = () => { const rootFiles = getFilesInDir(appDir); if (rootFiles.includes('tsconfig.json')) return 'ts' else return 'js' } /** * * @returns {string} */ const userTransformFilenameFn = (filename) => { const path = files.pathResolve(files.pathJoin(appDir, options.replaceFn)); const replaceFn = require(path).transformFilename; if (typeof replaceFn !== 'function') { Console.error(red`You must provide a valid function transformFilename.`); Console.error(yellow`The function should be named transformFilename and should be exported.`); throw new main.ExitWithCode(2); } return replaceFn(scaffoldName, filename); } /** * * @returns {string} */ const userTransformContentsFn = (contents, fileName) => { const path = files.pathResolve(files.pathJoin(appDir, options.replaceFn)); const replaceFn = require(path).transformContents; if (typeof replaceFn !== 'function') { Console.error(red`You must provide a valid function transformContents.`); Console.error(yellow`The function should be named transformContents and should be exported.`); throw new main.ExitWithCode(2); } return replaceFn(scaffoldName, contents, fileName); } /** * if contains - turns into pascal * @param str{string} * @returns {string} */ const toPascalCase = (str) => { if(!str.includes('-')) return str.charAt(0).toUpperCase() + str.slice(1); else return str.split('-').map(toPascalCase).join(''); } const toCamelCase = (str) => { if(!str.includes('-')) return str.charAt(0).toLowerCase() + str.slice(1); else return str.split('-').map(toPascalCase).join(''); } /** * * @param name {string} */ const transformName = (name) => { return name.replace(/\$\$name\$\$|\$\$PascalName\$\$|\$\$camelName\$\$/g, function (substring, args) { if (substring === '$$name$$') return scaffoldName; if (substring === '$$PascalName$$') return toPascalCase(scaffoldName); if (substring === '$$camelName$$') return toCamelCase(scaffoldName); }) } /** * * @param content{string} * @param fileName{string} * @returns {string} */ const removeUnusedLines = (content, fileName) => { if (methods && publications) return content; if (!methods && !publications) return content; if(!fileName.startsWith('index')) return content; return content .split('\n') .filter(line => { if (!methods && line.includes('methods')) return false; if (!publications && line.includes('publications')) return false; return true; }) .join('\n'); } /// Program const rootFiles = getFilesInDir(appDir); if (!rootFiles.includes('.meteor')) { Console.error(red`You must be in a Meteor project to run this command`); Console.error(yellow`You can create a new Meteor project with 'meteor create'`); throw new main.ExitWithCode(2); } const extension = getExtension() const assetsPath = () => { if (options.templatePath){ const templatePath = files.pathJoin(appDir, options.templatePath) Console.info(`Using template that is in: ${purple(templatePath)}`) return templatePath; } return files.pathJoin( __dirnameConverted, '..', 'static-assets', `scaffolds-${ extension }`) } // create directory const isOk = files.mkdir_p(scaffoldPath); if (!isOk) { Console.error(red`Something went wrong when creating the folder`); Console.error(yellow`Do you have the correct permissions?`); throw new main.ExitWithCode(2); } files.cp_r(assetsPath(), files.pathResolve(scaffoldPath), { transformFilename: function (f) { if (options.replaceFn) return userTransformFilenameFn(f); return transformName(f); }, transformContents: function (contents, fileName) { if (options.replaceFn) return userTransformContentsFn(contents.toString(), fileName); const cleaned = removeUnusedLines(contents.toString(), fileName); return transformName(cleaned); } }) const checkAndRemoveFiles = () => { if (!methods) files.unlink(files.pathJoin(scaffoldPath, `methods.${ extension }`)); if (!publications) files.unlink(files.pathJoin(scaffoldPath, `publications.${ extension }`)); } const xor = (a, b) => ( a || b ) && !( a && b ); if (!isWizard && xor(methods, publications)) { checkAndRemoveFiles() } if (isWizard) { checkAndRemoveFiles() } const packageJsonPath = files.pathJoin(appDir, 'package.json'); const packageJsonFile = files.readFile(packageJsonPath, 'utf8'); const packageJson = JSON.parse(packageJsonFile); const mainJsPath = packageJson?.meteor?.mainModule?.server ? files.pathJoin(appDir, packageJson.meteor.mainModule.server) : files.pathJoin(appDir, 'server', 'main.js'); const mainJs = files.readFile(mainJsPath); const mainJsLines = mainJs.toString().split('\n'); const importLine = path ? `import '${path}';` : `import '/imports/api/${ scaffoldName }';` const mainJsFile = [importLine, ...mainJsLines].join('\n'); files.writeFile(mainJsPath, mainJsFile); Console.info(`Created ${ blue(scaffoldName) } scaffold in ${ yellow(scaffoldPath) }`); return 0; }); /////////////////////////////////////////////////////////////////////////////// // admin get-machine /////////////////////////////////////////////////////////////////////////////// main.registerCommand({ name: 'admin get-machine', minArgs: 1, maxArgs: 1, options: { json: { type: Boolean }, verbose: { type: Boolean, short: "v" }, // By default, we give you a machine for 5 minutes. You can request up to // 15. (Meteor Software can reserve machines for longer than that.) minutes: { type: Number } }, pretty: false, catalogRefresh: new catalog.Refresh.Never() }, function (options) { Console.warn(); Console.warn("The 'meteor admin get-machine' command has been disabled and", "the build farm has been discontinued."); Console.warn(); Console.info("As of Meteor 1.4, packages with binary dependencies are", "automatically compiled when they are installed in an application,", "assuming the target machine has a basic compiler toolchain."); Console.info(); Console.info("To see the requirements for this compilation step,", "consult the platform requirements for 'node-gyp':"); Console.info( Console.url("https://github.com/nodejs/node-gyp"), Console.options({ indent: 2 }) ); Console.info(); return 1; }); /////////////////////////////////////////////////////////////////////////////// // admin progressbar-test /////////////////////////////////////////////////////////////////////////////// // A test command to print a progressbar. Useful for manual testing. main.registerCommand({ name: 'admin progressbar-test', options: { secs: { type: Number, default: 20 }, spinner: { type: Boolean, default: false } }, hidden: true, catalogRefresh: new catalog.Refresh.Never() }, function (options) { buildmessage.enterJob({ title: "A test progressbar" }, function () { var progress = buildmessage.getCurrentProgressTracker(); var totalProgress = { current: 0, end: options.secs, done: false }; var i = 0; var n = options.secs; if (options.spinner) { totalProgress.end = undefined; } new Promise(function (resolve) { function updateProgress() { i++; if (! options.spinner) { totalProgress.current = i; } if (i === n) { totalProgress.done = true; progress.reportProgress(totalProgress); resolve(); } else { progress.reportProgress(totalProgress); setTimeout(updateProgress, 1000); } } setTimeout(updateProgress); }).await(); }); }); /////////////////////////////////////////////////////////////////////////////// // dummy /////////////////////////////////////////////////////////////////////////////// // Dummy test command. Used for automated testing of the command line // option parser. main.registerCommand({ name: 'dummy', options: { ething: { type: String, short: "e", required: true }, port: { type: Number, short: "p", default: DEFAULT_PORT }, url: { type: Boolean, short: "U" }, 'delete': { type: Boolean, short: "D" }, changed: { type: Boolean } }, maxArgs: 2, hidden: true, pretty: false, catalogRefresh: new catalog.Refresh.Never() }, function (options) { var p = function (key) { if (_.has(options, key)) { return JSON.stringify(options[key]); } return 'none'; }; Console.info(p('ething') + " " + p('port') + " " + p('changed') + " " + p('args')); if (options.url) { Console.info('url'); } if (options['delete']) { Console.info('delete'); } }); /////////////////////////////////////////////////////////////////////////////// // throw-error /////////////////////////////////////////////////////////////////////////////// // Dummy test command. Used to test that stack traces work from an installed // Meteor tool. main.registerCommand({ name: 'throw-error', hidden: true, catalogRefresh: new catalog.Refresh.Never() }, function () { throw new Error("testing stack traces!"); // #StackTraceTest this line is found in tests/source-maps.js });