diff --git a/bin/generate-android-snapshot b/bin/generate-android-snapshot new file mode 100644 index 00000000..2aed1dde --- /dev/null +++ b/bin/generate-android-snapshot @@ -0,0 +1,6 @@ +#!/usr/bin/env node +const ProjectSnapshotGenerator = require("../snapshot/android/project-snapshot-generator"); +const args = require("../snapshot/android/project-snapshot-generator-cli-ags-parser")(); + +const generator = new ProjectSnapshotGenerator(args); +generator.generate(args); diff --git a/bin/generate-android-snapshot.cmd b/bin/generate-android-snapshot.cmd new file mode 100644 index 00000000..4f28f8e3 --- /dev/null +++ b/bin/generate-android-snapshot.cmd @@ -0,0 +1 @@ +@node %~dp0\generate-android-snapshot %* diff --git a/bin/ns-bundle b/bin/ns-bundle index 99d0dfe4..afc00541 100644 --- a/bin/ns-bundle +++ b/bin/ns-bundle @@ -4,6 +4,7 @@ const { spawn } = require("child_process"); const { resolve: pathResolve } = require("path"); const { existsSync } = require("fs"); const { getPackageJson } = require("../projectHelpers"); +const { isVersionGte } = require("../utils"); const PROJECT_DIR = pathResolve(__dirname, "../../../"); const packageJson = getPackageJson(PROJECT_DIR); @@ -15,6 +16,7 @@ if (!process.env.npm_config_argv) { const escape = arg => `"${arg}"`; const isTnsCommand = flag => flag.endsWith("-app"); const shouldUglify = () => process.env.npm_config_uglify; +const shouldSnapshot = (platform) => platform == "android" && require("os").type() != "Windows_NT" && process.env.npm_config_snapshot; const npmArgs = JSON.parse(process.env.npm_config_argv).original; const tnsArgs = getTnsArgs(npmArgs).map(escape); @@ -28,6 +30,7 @@ function getTnsArgs(args) { "--android", "--ios", "--uglify", + "--snapshot", "--nobundle", ]; @@ -37,24 +40,29 @@ function getTnsArgs(args) { execute(options); function execute(options) { - let commands = []; const platform = options.platform; + let commands = [ + () => runTns("prepare", platform) + ]; if (options.bundle) { commands = [ + ...commands, () => cleanApp(platform), + () => cleanSnapshotArtefacts(), () => cleanBuildArtifacts(platform), () => webpack(platform), ]; } + if (shouldSnapshot(platform)) { + commands.push(() => installSnapshotArtefacts()); + } + // If "build-app" or "start-app" is specified, // the respective command should be run last. - // Otherwise, the app should be just prepared. if (options.command) { commands.push(() => runTns(options.command, platform)); - } else { - commands.shift(() => runTns("prepare", platform)) } return commands.reduce((current, next) => current.then(next), Promise.resolve()); } @@ -65,12 +73,10 @@ function cleanBuildArtifacts(platform) { return resolve(); } - getTnsVersion().then(versionString => { - const version = versionToNumber(versionString); - + getTnsVersion().then(version => { // the android build artifacts should be cleaned manually // for nativescript-cli v3.0.1 and below or if using uglify - if (version <= 301 || shouldUglify()) { + if (isVersionGte(version, "3.0.1") || shouldUglify()) { gradlewClean().then(resolve).catch(throwError); } else { return resolve(); @@ -79,6 +85,14 @@ function cleanBuildArtifacts(platform) { }); } +function cleanSnapshotArtefacts() { + require("../snapshot/android/project-snapshot-generator").cleanSnapshotArtefacts(PROJECT_DIR); +} + +function installSnapshotArtefacts() { + require("../snapshot/android/project-snapshot-generator").installSnapshotArtefacts(PROJECT_DIR); +} + function gradlewClean() { return new Promise((resolve, reject) => { const platformsPath = pathResolve(PROJECT_DIR, "platforms", "android") @@ -135,6 +149,7 @@ function webpack(platform) { `--progress`, `--env.${platform}`, shouldUglify() && `--env.uglify`, + shouldSnapshot(platform) && `--env.snapshot` ]; spawnChildProcess(...args) diff --git a/bin/update-ns-webpack b/bin/update-ns-webpack index 3e5d3965..e5971284 100644 --- a/bin/update-ns-webpack +++ b/bin/update-ns-webpack @@ -1,14 +1,20 @@ #!/usr/bin/env node -const path = require("path"); -const fs = require("fs"); +const { resolve } = require("path"); -const helpers = require("../projectHelpers"); -const forceUpdateProjectDeps = require("../dependencyManager").forceUpdateProjectDeps; +const { getPackageJson, writePackageJson } = require("../projectHelpers"); +const { forceUpdateProjectDeps } = require("../dependencyManager"); +const { editExistingProjectFiles } = require("../projectFilesManager"); -const PROJECT_DIR = path.resolve(__dirname, "../../../"); -const packageJson = helpers.getPackageJson(PROJECT_DIR); +const PROJECT_DIR = resolve(__dirname, "../../../"); +console.info("Updating dev dependencies..."); +const packageJson = getPackageJson(PROJECT_DIR); const { deps } = forceUpdateProjectDeps(packageJson); packageJson.devDependencies = deps; +writePackageJson(packageJson, PROJECT_DIR); + +console.info("\nUpdating configuration files..."); +editExistingProjectFiles(PROJECT_DIR); + +console.info("\nProject successfully updated! Don't forget to run `npm install`"); -helpers.writePackageJson(packageJson, PROJECT_DIR); diff --git a/index.js b/index.js index 5557ec39..187863a9 100644 --- a/index.js +++ b/index.js @@ -1,84 +1,19 @@ -var sources = require("webpack-sources"); -var fs = require("fs"); -var path = require("path"); +const path = require("path"); +const { existsSync } = require("fs"); -var projectDir = path.dirname(path.dirname(__dirname)); -var packageJsonPath = path.join(projectDir, "package.json"); -var packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8")); +const { getPackageJson, isAngular } = require("./projectHelpers"); +const { sanitize } = require("./utils"); -var isAngular = Object.keys(packageJson.dependencies).filter(function (dependency) { - return /^@angular\b/.test(dependency); -}).length > 0; +const PROJECT_DIR = path.dirname(path.dirname(__dirname)); +const APP_DIR = path.join(PROJECT_DIR, "app"); +Object.assign(exports, require('./plugins')); -if (isAngular) { - exports.UrlResolvePlugin = require("./resource-resolver-plugins/UrlResolvePlugin"); +if (isAngular({projectDir: PROJECT_DIR})) { + Object.assign(exports, require('./plugins/angular')); } -//HACK: changes the JSONP chunk eval function to `global["nativescriptJsonp"]` -// applied to tns-java-classes.js only -exports.NativeScriptJsonpPlugin = function () { -}; - -exports.NativeScriptJsonpPlugin.prototype.apply = function (compiler) { - compiler.plugin("compilation", function (compilation) { - compilation.plugin("optimize-chunk-assets", function (chunks, callback) { - chunks.forEach(function (chunk) { - chunk.files.forEach(function (file) { - if (file === "vendor.js") { - var src = compilation.assets[file]; - var code = src.source(); - var match = code.match(/window\["nativescriptJsonp"\]/); - if (match) { - compilation.assets[file] = new sources.ConcatSource(code.replace(/window\["nativescriptJsonp"\]/g, "global[\"nativescriptJsonp\"]")); - } - } - }); - }); - callback(); - }); - }); -}; - -exports.GenerateBundleStarterPlugin = function (bundles) { - this.bundles = bundles; -}; - -exports.GenerateBundleStarterPlugin.prototype = { - apply: function (compiler) { - var plugin = this; - plugin.webpackContext = compiler.options.context; - - compiler.plugin("emit", function (compilation, cb) { - compilation.assets["package.json"] = plugin.generatePackageJson(); - compilation.assets["starter.js"] = plugin.generateStarterModule(); - plugin.generateTnsJavaClasses(compilation); - - cb(); - }); - }, - generateTnsJavaClasses: function (compilation) { - const path = compilation.compiler.outputPath; - const isAndroid = path.indexOf("android") > -1; - - if (isAndroid) { - compilation.assets["tns-java-classes.js"] = new sources.RawSource(""); - } - }, - generatePackageJson: function () { - var packageJsonPath = path.join(this.webpackContext, "package.json"); - var packageData = JSON.parse(fs.readFileSync(packageJsonPath, "utf8")); - packageData.main = "starter"; - - return new sources.RawSource(JSON.stringify(packageData, null, 4)); - }, - generateStarterModule: function () { - var moduleSource = this.bundles.map(function (bundle) { - return "require(\"" + bundle + "\");"; - }).join("\n"); - return new sources.RawSource(moduleSource); - }, -}; +exports.uglifyMangleExcludes = require("./mangle-excludes"); exports.getEntryModule = function () { const maybePackageJsonEntry = getPackageJsonEntry(); @@ -90,46 +25,29 @@ exports.getEntryModule = function () { return maybeAotEntry || maybePackageJsonEntry; }; -exports.getAppPath = function (platform) { - var projectDir = path.dirname(path.dirname(__dirname)); - +exports.getAppPath = platform => { if (/ios/i.test(platform)) { - var appName = path.basename(projectDir); - var sanitizedName = appName.split("").filter(function (c) { - return /[a-zA-Z0-9]/.test(c); - }).join(""); - return "platforms/ios/" + sanitizedName + "/app"; + const appName = path.basename(PROJECT_DIR); + const sanitizedName = sanitize(appName); + + return `platforms/ios/${sanitizedName}/app`; } else if (/android/i.test(platform)) { - return path.join(projectDir, "platforms/android/src/main/assets/app"); + return path.join(PROJECT_DIR, "platforms/android/src/main/assets/app"); } else { - throw new Error("Invalid platform: " + platform); + throw new Error(`Invalid platform: ${platform}`); } }; -exports.uglifyMangleExcludes = require("./mangle-excludes"); - function getPackageJsonEntry() { - const packageJsonSource = getAppPackageJsonSource(); + const packageJsonSource = getPackageJson(APP_DIR); const entry = packageJsonSource.main; return entry ? entry.replace(/\.js$/i, "") : null; } -function getAppPackageJsonSource() { - const projectDir = getProjectDir(); - const appPackageJsonPath = path.join(projectDir, "app", "package.json"); - - return JSON.parse(fs.readFileSync(appPackageJsonPath, "utf8")); -} - function getAotEntry(entry) { const aotEntry = `${entry}.aot.ts`; - const projectDir = getProjectDir(); - const aotEntryPath = path.join(projectDir, "app", aotEntry); - - return fs.existsSync(aotEntryPath) ? aotEntry : null; -} + const aotEntryPath = path.join(APP_DIR, aotEntry); -function getProjectDir() { - return path.dirname(path.dirname(__dirname)); + return existsSync(aotEntryPath) ? aotEntry : null; } diff --git a/installer.js b/installer.js index 7bc83dd3..1869aa72 100644 --- a/installer.js +++ b/installer.js @@ -13,7 +13,6 @@ function install() { let packageJson = helpers.getPackageJson(PROJECT_DIR); projectFilesManager.addProjectFiles(PROJECT_DIR, APP_DIR); - projectFilesManager.editExistingProjectFiles(PROJECT_DIR); let scripts = packageJson.scripts || {}; scripts = npmScriptsManager.removeDeprecatedNpmScripts(scripts); diff --git a/npmScriptsManager.js b/npmScriptsManager.js index af5d99cd..6a0a1563 100644 --- a/npmScriptsManager.js +++ b/npmScriptsManager.js @@ -3,6 +3,7 @@ const SCRIPT_TEMPLATES = Object.freeze({ "start-[PLATFORM]-bundle": "npm run ns-bundle --[PLATFORM] --run-app", "build-[PLATFORM]-bundle": "npm run ns-bundle --[PLATFORM] --build-app", "publish-ios-bundle": "npm run ns-bundle --ios --publish-app", + "generate-android-snapshot": "generate-android-snapshot --projectRoot . --targetArchs arm,arm64 --install" }); const DEPRECATED_SCRIPT_TEMPLATES = Object.freeze([ diff --git a/package.json b/package.json index d025344a..3d759ea8 100644 --- a/package.json +++ b/package.json @@ -22,8 +22,12 @@ "remove-ns-webpack": "./bin/remove-ns-webpack", "update-ns-webpack": "./bin/update-ns-webpack", "ns-bundle": "./bin/ns-bundle", - "ns-verify-bundle": "./bin/ns-verify-bundle" + "ns-verify-bundle": "./bin/ns-verify-bundle", + "generate-android-snapshot": "./bin/generate-android-snapshot" + }, + "dependencies": { + "adm-zip": "^0.4.7", + "shelljs": "^0.6.0" }, - "dependencies": {}, "devDependencies": {} } diff --git a/plugins/GenerateBundleStarterPlugin.js b/plugins/GenerateBundleStarterPlugin.js new file mode 100644 index 00000000..f97127a8 --- /dev/null +++ b/plugins/GenerateBundleStarterPlugin.js @@ -0,0 +1,47 @@ +const { RawSource } = require("webpack-sources"); +const { getPackageJson } = require("../projectHelpers"); + +exports.GenerateBundleStarterPlugin = (function() { + function GenerateBundleStarterPlugin(bundles) { + this.bundles = bundles; + }; + + GenerateBundleStarterPlugin.prototype.apply = function(compiler) { + const plugin = this; + plugin.webpackContext = compiler.options.context; + + compiler.plugin("emit", function (compilation, cb) { + compilation.assets["package.json"] = plugin.generatePackageJson(); + compilation.assets["starter.js"] = plugin.generateStarterModule(); + plugin.generateTnsJavaClasses(compilation); + + cb(); + }); + } + + GenerateBundleStarterPlugin.prototype.generateTnsJavaClasses = function (compilation) { + const path = compilation.compiler.outputPath; + const isAndroid = path.indexOf("android") > -1; + + if (isAndroid && !compilation.assets["tns-java-classes.js"]) { + compilation.assets["tns-java-classes.js"] = new RawSource(""); + } + } + + GenerateBundleStarterPlugin.prototype.generatePackageJson = function () { + const packageJson = getPackageJson(this.webpackContext); + packageJson.main = "starter"; + + return new RawSource(JSON.stringify(packageJson, null, 4)); + } + + GenerateBundleStarterPlugin.prototype.generateStarterModule = function () { + const moduleSource = this.bundles + .map(bundle => `require("${bundle}")`) + .join("\n"); + + return new RawSource(moduleSource); + } + + return GenerateBundleStarterPlugin; +})(); diff --git a/plugins/NativeScriptJsonpPlugin.js b/plugins/NativeScriptJsonpPlugin.js new file mode 100644 index 00000000..a75ba42b --- /dev/null +++ b/plugins/NativeScriptJsonpPlugin.js @@ -0,0 +1,38 @@ +const { ConcatSources } = require("webpack-sources"); + +const WINDOWS_GLOBAL_MATCHER = /window\["nativescriptJsonp"\]/g; +const NATIVESCRIPT_GLOBAL = "global[\"nativescriptJsonp\"]"; +const isVendorChunk = name => name === "vendor.js"; + +//HACK: changes the JSONP chunk eval function to `global["nativescriptJsonp"]` +// applied to tns-java-classes.js only +exports.NativeScriptJsonpPlugin = (function() { + function NativeScriptJsonpPlugin() { + } + + NativeScriptJsonpPlugin.prototype.apply = function (compiler) { + compiler.plugin("compilation", function (compilation) { + compilation.plugin("optimize-chunk-assets", function (chunks, callback) { + chunks.forEach(function (chunk) { + chunk.files + .filter(isVendorChunk) + .forEach(file => replaceGlobal(compilation.assets, file)); + }); + callback(); + }); + }); + }; + + return NativeScriptJsonpPlugin; +})(); + +function replaceGlobal(assets, file) { + const path = assets[file]; + const source = path.source(); + const match = source.match(WINDOWS_GLOBAL_MATCHER); + + if (match) { + const newSource = source.replace(WINDOWS_GLOBAL_MATCHER, NATIVESCRIPT_GLOBAL); + assets[file] = new ConcatSource(newSource); + } +} diff --git a/plugins/NativeScriptSnapshotPlugin.js b/plugins/NativeScriptSnapshotPlugin.js new file mode 100644 index 00000000..d90c6e9c --- /dev/null +++ b/plugins/NativeScriptSnapshotPlugin.js @@ -0,0 +1,88 @@ +const { resolve, join } = require("path"); +const { closeSync, openSync } = require("fs"); + +const ProjectSnapshotGenerator = require("../snapshot/android/project-snapshot-generator"); + +exports.NativeScriptSnapshotPlugin = (function() { + function NativeScriptSnapshotPlugin(options) { + ProjectSnapshotGenerator.call(this, options); // Call the parent constructor + + if (!this.options.chunk) { + throw new Error("No chunk specified."); + } + + console.dir() + + if (this.options.webpackConfig) { + if (this.options.webpackConfig.output && this.options.webpackConfig.output.libraryTarget) { + this.options.webpackConfig.output.libraryTarget = undefined; + } + + if (this.options.webpackConfig.entry) { + if (typeof this.options.webpackConfig.entry === "string" || + this.options.webpackConfig.entry instanceof Array) + this.options.webpackConfig.entry = { bundle: this.options.webpackConfig.entry }; + } + + this.options.webpackConfig.entry["tns-java-classes"] = this.getTnsJavaClassesBuildPath(); + } + } + + // inherit ProjectSnapshotGenerator + NativeScriptSnapshotPlugin.prototype = Object.create(ProjectSnapshotGenerator.prototype); + NativeScriptSnapshotPlugin.prototype.constructor = NativeScriptSnapshotPlugin; + + NativeScriptSnapshotPlugin.prototype.getTnsJavaClassesBuildPath = function() { + return resolve(this.getBuildPath(), "../tns-java-classes.js"); + } + + NativeScriptSnapshotPlugin.prototype.generate = function(webpackChunk) { + const options = this.options; + + const inputFile = join(options.webpackConfig.output.path, webpackChunk.files[0]); + + console.log(`\n Snapshotting bundle at ${inputFile}`); + + const preparedAppRootPath = join(options.projectRoot, "platforms/android/src/main/assets"); + const preprocessedInputFile = join(preparedAppRootPath, "app/_embedded_script_.js"); + + ProjectSnapshotGenerator.prototype.generate.call(this, { + inputFile, + preprocessedInputFile, + targetArchs: options.targetArchs, + useLibs: options.useLibs, + androidNdkPath: options.androidNdkPath, + tnsJavaClassesPath: join(preparedAppRootPath, "app/tns-java-classes.js") + }); + + // Make the original file empty + if (inputFile !== preprocessedInputFile) { + closeSync(openSync(inputFile, "w")); // truncates the input file content + } + } + + NativeScriptSnapshotPlugin.prototype.apply = function(compiler) { + const options = this.options; + + // Generate tns-java-classes.js file + debugger; + ProjectSnapshotGenerator.prototype.generateTnsJavaClassesFile.call(this, { + output: this.getTnsJavaClassesBuildPath(), + options: options.tnsJavaClassesOptions + }); + + // Run the snapshot tool when the packing is done + compiler.plugin("done", function(result) { + debugger; + const chunkToSnapshot = result.compilation.chunks.find(chunk => chunk.name == options.chunk); + if (!chunkToSnapshot) { + throw new Error(`No chunk named '${options.chunk}' found.`); + } + + this.generate(chunkToSnapshot); + + }.bind(this)); + } + + return NativeScriptSnapshotPlugin; +})(); diff --git a/resource-resolver-plugins/UrlResolvePlugin.js b/plugins/UrlResolvePlugin.js similarity index 87% rename from resource-resolver-plugins/UrlResolvePlugin.js rename to plugins/UrlResolvePlugin.js index 1222d695..cf07a13c 100644 --- a/resource-resolver-plugins/UrlResolvePlugin.js +++ b/plugins/UrlResolvePlugin.js @@ -1,8 +1,8 @@ -const ts = require("typescript"); -const fs = require("fs"); -const path = require("path"); +const { forEachChild, SyntaxKind } = require("typescript"); +const { existsSync } = require("fs"); +const { resolve } = require("path"); -const UrlResolvePlugin = (function() { +exports.UrlResolvePlugin = (function() { function UrlResolvePlugin(options) { if (!options || !options.platform) { throw new Error(`Target platform must be specified!`); @@ -40,15 +40,15 @@ const UrlResolvePlugin = (function() { UrlResolvePlugin.prototype.usePlatformUrl = function(sourceFile) { this.setCurrentDirectory(sourceFile); - ts.forEachChild(sourceFile, node => this.traverseDecorators(node)); + forEachChild(sourceFile, node => this.traverseDecorators(node)); } UrlResolvePlugin.prototype.setCurrentDirectory = function(sourceFile) { - this.currentDirectory = path.resolve(sourceFile.path, ".."); + this.currentDirectory = resolve(sourceFile.path, ".."); } UrlResolvePlugin.prototype.traverseDecorators = function(node) { - if (node.kind !== ts.SyntaxKind.ClassDeclaration || !node.decorators) { + if (node.kind !== SyntaxKind.ClassDeclaration || !node.decorators) { return; } @@ -90,9 +90,9 @@ const UrlResolvePlugin = (function() { } UrlResolvePlugin.prototype.noMultiplatformFile = function(url) { - let filePath = path.resolve(this.currentDirectory, url); + let filePath = resolve(this.currentDirectory, url); - return !fs.existsSync(filePath); + return !existsSync(filePath); } UrlResolvePlugin.prototype.replaceUrlsValue = function(element) { @@ -105,5 +105,3 @@ const UrlResolvePlugin = (function() { return UrlResolvePlugin; })(); - -module.exports = UrlResolvePlugin; diff --git a/plugins/angular.js b/plugins/angular.js new file mode 100644 index 00000000..3a406058 --- /dev/null +++ b/plugins/angular.js @@ -0,0 +1,4 @@ +module.exports = Object.assign({}, + require("./UrlResolvePlugin") +); + diff --git a/plugins/index.js b/plugins/index.js new file mode 100644 index 00000000..114e8877 --- /dev/null +++ b/plugins/index.js @@ -0,0 +1,5 @@ +module.exports = Object.assign({}, + require("./GenerateBundleStarterPlugin"), + require("./NativeScriptJsonpPlugin"), + require("./NativeScriptSnapshotPlugin") +); diff --git a/prepublish/common/exports.js b/prepublish/common/exports.js index 0ba564b0..739e8905 100644 --- a/prepublish/common/exports.js +++ b/prepublish/common/exports.js @@ -22,7 +22,7 @@ module.exports = env => { const plugins = getPlugins(platform, env); const extensions = getExtensions(platform); - return { + const config = { context: resolve("./app"), target: nativescriptTarget, entry, @@ -51,6 +51,18 @@ module.exports = env => { module: { rules }, plugins, }; + + if (env.snapshot) { + plugins.push(new nsWebpack.NativeScriptSnapshotPlugin({ + chunk: "vendor", + projectRoot: __dirname, + webpackConfig: config, + targetArchs: ["arm", "arm64"], + tnsJavaClassesOptions: { packages: ["tns-core-modules" ] }, + useLibs: false + })); + } + + return config; }; `; - diff --git a/prepublish/common/uglify.js b/prepublish/common/uglify.js index ef57ad82..2b6bcf96 100644 --- a/prepublish/common/uglify.js +++ b/prepublish/common/uglify.js @@ -1,5 +1,4 @@ -module.exports = ` - if (env.uglify) { +module.exports = `if (env.uglify) { plugins.push(new webpack.LoaderOptionsPlugin({ minimize: true })); // Work around an Android issue by setting compress = false @@ -8,4 +7,5 @@ module.exports = ` mangle: { except: nsWebpack.uglifyMangleExcludes }, compress, })); - }`; + } +`; diff --git a/prepublish/index.js b/prepublish/index.js index e4abb019..2795699a 100644 --- a/prepublish/index.js +++ b/prepublish/index.js @@ -51,10 +51,11 @@ function isArray(resource) { function pluginsBuilder(plugins) { const uglify = require("./common/uglify"); + return `function getPlugins(platform, env) { let plugins = ${plugins}; - ${uglify} + ${uglify} return plugins; }\n`; } diff --git a/projectFilesManager.js b/projectFilesManager.js index b7c298ea..7c38766b 100644 --- a/projectFilesManager.js +++ b/projectFilesManager.js @@ -1,7 +1,35 @@ const path = require("path"); const fs = require("fs"); -const helpers = require("./projectHelpers"); +const { isTypeScript, isAngular } = require("./projectHelpers"); + +const FRAME_MATCH = /(\s*)(require\("ui\/frame"\);)(\s*)(require\("ui\/frame\/activity"\);)/g; +const SCOPED_FRAME = ` +if (!global["__snapshot"]) { + // In case snapshot generation is enabled these modules will get into the bundle + // but will not be required/evaluated. + // The snapshot webpack plugin will add them to the tns-java-classes.js bundle file. + // This way, they will be evaluated on app start as early as possible. +$1\t$2$3\t$4 +}`; + +const CONFIG_MATCH = /(exports = [^]+?)\s*return ({[^]+target:\s*nativescriptTarget[^]+?};)/; +const CONFIG_REPLACE = `$1 + + const config = $2 + + if (env.snapshot) { + plugins.push(new nsWebpack.NativeScriptSnapshotPlugin({ + chunk: "vendor", + projectRoot: __dirname, + webpackConfig: config, + targetArchs: ["arm", "arm64"], + tnsJavaClassesOptions: { packages: ["tns-core-modules" ] }, + useLibs: false + })); + } + + return config;`; function addProjectFiles(projectDir, appDir) { const projectTemplates = getProjectTemplates(projectDir); @@ -51,10 +79,10 @@ function copyTemplate(templateName, destinationPath) { function getProjectTemplates(projectDir) { let templates = {} - if (helpers.isAngular({projectDir})) { + if (isAngular({projectDir})) { templates["webpack.angular.js"] = "webpack.config.js"; templates["tsconfig.aot.json"] = "tsconfig.aot.json"; - } else if (helpers.isTypeScript({projectDir})) { + } else if (isTypeScript({projectDir})) { templates["webpack.typescript.js"] = "webpack.config.js"; } else { templates["webpack.javascript.js"] = "webpack.config.js"; @@ -69,7 +97,7 @@ function getAppTemplates(projectDir, appDir) { "vendor-platform.ios.ts": tsOrJs(projectDir, "vendor-platform.ios"), }; - if (helpers.isAngular({projectDir})) { + if (isAngular({projectDir})) { templates["vendor.angular.ts"] = tsOrJs(projectDir, "vendor"); } else { templates["vendor.nativescript.ts"] = tsOrJs(projectDir, "vendor"); @@ -95,31 +123,67 @@ function editExistingProjectFiles(projectDir) { const webpackConfigPath = getFullPath(projectDir, "webpack.config.js"); const webpackCommonPath = getFullPath(projectDir, "webpack.common.js"); - editWebpackConfig(webpackConfigPath, replaceStyleUrlResolvePlugin); - editWebpackConfig(webpackCommonPath, replaceStyleUrlResolvePlugin); + const configChangeFunctions = [ + replaceStyleUrlResolvePlugin, + addSnapshotPlugin, + ]; + + editFileContent(webpackConfigPath, ...configChangeFunctions); + editFileContent(webpackCommonPath, ...configChangeFunctions); + + const extension = isAngular({projectDir}) ? "ts" : "js"; + const vendorAndroidPath = getFullPath( + projectDir, + `app/vendor-platform.android.${extension}` + ); + + editFileContent(vendorAndroidPath, addSnapshotToVendor); } -function editWebpackConfig(path, fn) { +function editFileContent(path, ...funcs) { if (!fs.existsSync(path)) { return; } - const config = fs.readFileSync(path, "utf8"); - const newConfig = fn(config); + let content = fs.readFileSync(path, "utf8"); + funcs.forEach(fn => content = fn(content)); - fs.writeFileSync(path, newConfig, "utf8"); + fs.writeFileSync(path, content, "utf8"); } function replaceStyleUrlResolvePlugin(config) { + if (config.indexOf("StyleUrlResolvePlugin") === -1) { + return config; + } + + console.info("Replacing deprecated StyleUrlsResolvePlugin with UrlResolvePlugin..."); return config.replace(/StyleUrlResolvePlugin/g, "UrlResolvePlugin"); } +function addSnapshotPlugin(config) { + if (config.indexOf("NativeScriptSnapshotPlugin") > -1) { + return config; + } + + console.info("Adding NativeScriptSnapshotPlugin configuration..."); + return config.replace(CONFIG_MATCH, CONFIG_REPLACE); +} + +function addSnapshotToVendor(content) { + if (content.indexOf("__snapshot") > -1) { + return content; + } + + console.info("Adding __snapshot configuration to app/vendor-platform.android ..."); + return content.replace(FRAME_MATCH, SCOPED_FRAME); +} + function getFullPath(projectDir, filePath) { return path.resolve(projectDir, filePath); } function tsOrJs(projectDir, name) { - const extension = helpers.isTypeScript({projectDir}) ? "ts" : "js"; + const extension = isTypeScript({projectDir}) ? "ts" : "js"; return `${name}.${extension}`; } diff --git a/snapshot/android/project-snapshot-generator-cli-ags-parser.js b/snapshot/android/project-snapshot-generator-cli-ags-parser.js new file mode 100644 index 00000000..5207e075 --- /dev/null +++ b/snapshot/android/project-snapshot-generator-cli-ags-parser.js @@ -0,0 +1,72 @@ +module.exports = function parseProjectSnapshotGeneratorArgs() { + var result = parseJsonFromProcessArgs(); + + if (result.targetArchs) { + result.targetArchs = parseStringArray(result.targetArchs); + } + if (result.tnsJavaClassesOptions && result.tnsJavaClassesOptions.packages !== undefined) { + result.tnsJavaClassesOptions.packages = parseStringArray(result.tnsJavaClassesOptions.packages); + } + if (result.tnsJavaClassesOptions && result.tnsJavaClassesOptions.modules !== undefined) { + result.tnsJavaClassesOptions.modules = parseStringArray(result.tnsJavaClassesOptions.modules); + } + + if (result.useLibs !== undefined) { + result.useLibs = parseBool(result.useLibs); + } + + if (result.install !== undefined) { + result.install = parseBool(result.install); + } + + return result; +} + +function parseJsonFromProcessArgs() { + var args = process.argv.slice(2); + var result = {}; + + var currentKey = ""; + var currentValue = ""; + args.forEach(function(value, index, array) { + if (value.startsWith("--")) { // if is key + addKeyAndValueToResult(currentKey, currentValue, result); + currentKey = value.slice(2); + currentValue = null; + } + else { // if is first value + currentValue = (currentValue === null) ? value : currentValue + " " + value; + } + + if (index == array.length - 1) { // if is the last one + addKeyAndValueToResult(currentKey, currentValue, result); + } + }); + + return result; +} + +function addKeyAndValueToResult(key, value, result) { + if (!key) + return; + var jsValue = (value === null) ? null : value.toString(); + var keyPath = key.split("."); + var parentObject = result; + for (var i = 0; i < keyPath.length; i++) { + if (i == keyPath.length - 1) { + parentObject[keyPath[i]] = jsValue; + } + else { + parentObject[keyPath[i]] = parentObject[keyPath[i]] || {}; + parentObject = parentObject[keyPath[i]]; + } + } +} + +function parseBool(value) { + return (value === null || value === "true") ? true : false; +} + +function parseStringArray(str) { + return str.toString().split(",").map(value => value.trim()); +} diff --git a/snapshot/android/project-snapshot-generator.js b/snapshot/android/project-snapshot-generator.js new file mode 100644 index 00000000..f159f1a4 --- /dev/null +++ b/snapshot/android/project-snapshot-generator.js @@ -0,0 +1,182 @@ +const path = require("path"); +const fs = require("fs"); +const shelljs = require("shelljs"); + +const SnapshotGenerator = require("./snapshot-generator"); +const TnsJavaClassesGenerator = require("./tns-java-classes-generator"); +const { isVersionGte } = require("../../utils"); +const { getPackageJson } = require("../../projectHelpers"); + +const MIN_ANDROID_RUNTIME_VERSION = "3.0.0"; +const VALID_ANDROID_RUNTIME_TAGS = Object.freeze(["next", "rc"]); + +function ProjectSnapshotGenerator (options) { + this.options = options = options || {}; + + options.projectRoot = options.projectRoot ? + (path.isAbsolute(options.projectRoot) ? + options.projectRoot : + path.resolve(process.cwd(), options.projectRoot)) : + process.cwd(); + + if (!options.projectRoot) { + throw new Error("The project root is not specified."); + } + + console.log("Project root: " + options.projectRoot); + console.log("Snapshots build directory: " + this.getBuildPath()); + + this.validateAndroidRuntimeVersion(); +} +module.exports = ProjectSnapshotGenerator; + +ProjectSnapshotGenerator.calculateBuildPath = function(projectRoot) { + return path.join(projectRoot, "platforms/android/snapshot-build/build"); +} + +ProjectSnapshotGenerator.prototype.getBuildPath = function() { + return ProjectSnapshotGenerator.calculateBuildPath(this.options.projectRoot); +} + +ProjectSnapshotGenerator.cleanSnapshotArtefacts = function(projectRoot) { + const platformPath = path.join(projectRoot, "platforms/android"); + + // Remove blob files from prepared folder + shelljs.rm("-rf", path.join(platformPath, "src/main/assets/snapshots")); + + // Remove prepared include.gradle configurations + shelljs.rm("-rf", path.join(platformPath, "configurations/", SnapshotGenerator.SNAPSHOT_PACKAGE_NANE)); +} + +ProjectSnapshotGenerator.installSnapshotArtefacts = function(projectRoot) { + const buildPath = ProjectSnapshotGenerator.calculateBuildPath(projectRoot); + const assetsPath = path.join(projectRoot, "platforms/android/src/main/assets"); + + // Copy tns-java-classes.js + if (shelljs.test("-e", path.join(buildPath, "tns-java-classes.js"))) { + shelljs.cp(path.join(buildPath, "tns-java-classes.js"), path.join(assetsPath, "app/tns-java-classes.js")); + } + + if (shelljs.test("-e", path.join(buildPath, "ndk-build/libs"))) { + // useLibs = true + const libsDestinationPath = path.join(projectRoot, "platforms/android/src", SnapshotGenerator.SNAPSHOT_PACKAGE_NANE, "jniLibs"); + const configDestinationPath = path.join(projectRoot, "platforms/android/configurations", SnapshotGenerator.SNAPSHOT_PACKAGE_NANE); + + // Copy the libs to the specified destination in the platforms folder + shelljs.mkdir("-p", libsDestinationPath); + shelljs.cp("-R", path.join(buildPath, "ndk-build/libs") + "/", libsDestinationPath); + + // Copy include.gradle to the specified destination in the platforms folder + shelljs.mkdir("-p", configDestinationPath); + shelljs.cp(path.join(buildPath, "include.gradle"), path.join(configDestinationPath, "include.gradle")); + } + else { + // useLibs = false + const blobsSrcPath = path.join(buildPath, "snapshots/blobs"); + const blobsDestinationPath = path.join(assetsPath, "snapshots"); + const appPackageJsonPath = path.join(assetsPath, "app/package.json"); + + // Copy the blobs in the prepared app folder + shelljs.cp("-R", blobsSrcPath + "/", path.join(assetsPath, "snapshots")); + + /* + Rename TNSSnapshot.blob files to snapshot.blob files. The xxd tool uses the file name for the name of the static array. This is why the *.blob files are initially named TNSSnapshot.blob. After the xxd step, they must be renamed to snapshot.blob, because this is the filename that the Android runtime is looking for. + */ + shelljs.exec("find " + blobsDestinationPath + " -name '*.blob' -execdir mv {} snapshot.blob ';'"); + + // Update the package.json file + var appPackageJson = shelljs.test("-e", appPackageJsonPath) ? JSON.parse(fs.readFileSync(appPackageJsonPath, 'utf8')) : {}; + appPackageJson["android"] = appPackageJson["android"] || {}; + appPackageJson["android"]["heapSnapshotBlob"] = "../snapshots"; + fs.writeFileSync(appPackageJsonPath, JSON.stringify(appPackageJson, null, 2)); + } +} + +ProjectSnapshotGenerator.prototype.getV8Version = function() { + const nativescriptLibraryPath = path.join(this.options.projectRoot, "platforms/android/libs/runtime-libs/nativescript-regular.aar"); + if (!fs.existsSync(nativescriptLibraryPath)) { + nativescriptLibraryPath = path.join(options.projectRoot, "platforms/android/libs/runtime-libs/nativescript.aar"); + } + + const zip = new require("adm-zip")(nativescriptLibraryPath); + const config = zip.readAsText("config.json"); + return config ? JSON.parse(config)["v8-version"] : "4.7.80"; +} + +ProjectSnapshotGenerator.prototype.getAndroidRuntimeVersion = function() { + try { + const projectPackageJSON = getPackageJson(this.options.projectRoot); + + return projectPackageJSON["nativescript"]["tns-android"]["version"]; + } catch(e) { + return null; + } +} + +ProjectSnapshotGenerator.prototype.validateAndroidRuntimeVersion = function() { + const currentRuntimeVersion = this.getAndroidRuntimeVersion(); + + if (!currentRuntimeVersion || + !fs.existsSync(path.join(this.options.projectRoot, "platforms/android"))) { + + throw new Error("In order to generate a V8 snapshot you must have the \"android\" platform installed - to do so please run \"tns platform add android\"."); + } + + if (!VALID_ANDROID_RUNTIME_TAGS.includes(currentRuntimeVersion) && + !isVersionGte(currentRuntimeVersion, MIN_ANDROID_RUNTIME_VERSION)) { + + throw new Error("In order to support heap snapshots, you must have at least tns-android@" + MIN_ANDROID_RUNTIME_VERSION + + " installed. Current Android Runtime version is: " + currentRuntimeVersion + "."); + } +} + +ProjectSnapshotGenerator.prototype.generateTnsJavaClassesFile = function(generationOptions) { + const tnsJavaClassesGenerator = new TnsJavaClassesGenerator(); + return tnsJavaClassesGenerator.generate({ + projectRoot: this.options.projectRoot, + output: generationOptions.output, + options: generationOptions.options + }); +} + +ProjectSnapshotGenerator.prototype.generate = function(generationOptions) { + generationOptions = generationOptions || {}; + + console.log("Running snapshot generation with the following arguments: "); + console.log(JSON.stringify(generationOptions, null, '\t')); + + // Clean build folder + shelljs.rm("-rf", this.getBuildPath()); + shelljs.mkdir("-p", this.getBuildPath()); + + // Generate tns-java-classes.js if needed + var tnsJavaClassesDestination = path.join(this.getBuildPath(), "tns-java-classes.js"); + if (generationOptions.tnsJavaClassesPath) { + if (generationOptions.tnsJavaClassesPath != tnsJavaClassesDestination) { + shelljs.cp(generationOptions.tnsJavaClassesPath, tnsJavaClassesDestination); + } + } + else { + this.generateTnsJavaClassesFile({ output: tnsJavaClassesDestination, options: generationOptions.tnsJavaClassesOptions }); + } + + // Generate snapshots + const generator = new SnapshotGenerator({ buildPath: this.getBuildPath() }); + const generatorBuildPath = generator.generate({ + inputFile: generationOptions.inputFile || path.join(this.options.projectRoot, "__snapshot.js"), + targetArchs: generationOptions.targetArchs || ["arm", "arm64", "ia32"], + v8Version: generationOptions.v8Version || this.getV8Version(), + preprocessedInputFile: generationOptions.preprocessedInputFile, + useLibs: generationOptions.useLibs || false, + androidNdkPath: generationOptions.androidNdkPath + }); + + if (generationOptions.install) { + ProjectSnapshotGenerator.cleanSnapshotArtefacts(this.options.projectRoot); + ProjectSnapshotGenerator.installSnapshotArtefacts(this.options.projectRoot); + } + + console.log(generationOptions.useLibs ? + "Snapshot is included in the app as dynamically linked library (.so file)." : + "Snapshot is included in the app as binary .blob file. The more space-efficient option is to embed it in a dynamically linked library (.so file)."); +} diff --git a/snapshot/android/snapshot-generator-tools/bundle-preamble.js b/snapshot/android/snapshot-generator-tools/bundle-preamble.js new file mode 100644 index 00000000..cef3b3b6 --- /dev/null +++ b/snapshot/android/snapshot-generator-tools/bundle-preamble.js @@ -0,0 +1,27 @@ +/*!**********************************************************!*\ + !*** ../android-snapshot-bundle-preamble.js ***! + \**********************************************************/ +var global = Function('return this')(); global.global = global; // Mock global object +// Set the __snapshot flag to true +Object.defineProperty(global, "__snapshot", { + value: true, + writable: false, + configurable: false, + enumerable: false +}); + +global.__requireOverride = (function() { + return function(moduleId, dirname) { + /* + The android runtime laods in advance all JS modules that contain a native class successor generated statically at build time. + In case of snapshot this file always is the bundled one. Since it is snapshoted it is already laoded in the heap and is not meant + to be required. The tns-java-classes.js file is responsible for actually executing the modules containing native java classes. + */ + var resolvedModuleId = moduleId.replace(/^\.\/tns_modules\//, ""); + if (resolvedModuleId === './_embedded_script_.js') { + return {}; + } + }; +}()); + + diff --git a/snapshot/android/snapshot-generator-tools/include.gradle b/snapshot/android/snapshot-generator-tools/include.gradle new file mode 100644 index 00000000..739ac63a --- /dev/null +++ b/snapshot/android/snapshot-generator-tools/include.gradle @@ -0,0 +1,13 @@ +android { + productFlavors { + "nativescript-android-snapshot" { + dimension "nativescript-android-snapshot" + } + } +} + +android { + aaptOptions { + ignoreAssetsPattern "_embedded_script_.js" + } +} diff --git a/snapshot/android/snapshot-generator-tools/mksnapshot/README.md b/snapshot/android/snapshot-generator-tools/mksnapshot/README.md new file mode 100644 index 00000000..74e15655 --- /dev/null +++ b/snapshot/android/snapshot-generator-tools/mksnapshot/README.md @@ -0,0 +1 @@ +This is the [tool used to generate V8 heap snapshots](https://github.com/NativeScript/android-v8/blob/4.7.80/tools/gyp/v8.gyp#L2072-L2095). diff --git a/snapshot/android/snapshot-generator-tools/mksnapshot/mksnapshot-5.5.372.32/darwin-x64/mksnapshot-arm b/snapshot/android/snapshot-generator-tools/mksnapshot/mksnapshot-5.5.372.32/darwin-x64/mksnapshot-arm new file mode 100755 index 00000000..48e7b10e Binary files /dev/null and b/snapshot/android/snapshot-generator-tools/mksnapshot/mksnapshot-5.5.372.32/darwin-x64/mksnapshot-arm differ diff --git a/snapshot/android/snapshot-generator-tools/mksnapshot/mksnapshot-5.5.372.32/darwin-x64/mksnapshot-arm64 b/snapshot/android/snapshot-generator-tools/mksnapshot/mksnapshot-5.5.372.32/darwin-x64/mksnapshot-arm64 new file mode 100755 index 00000000..ffa30751 Binary files /dev/null and b/snapshot/android/snapshot-generator-tools/mksnapshot/mksnapshot-5.5.372.32/darwin-x64/mksnapshot-arm64 differ diff --git a/snapshot/android/snapshot-generator-tools/mksnapshot/mksnapshot-5.5.372.32/darwin-x64/mksnapshot-ia32 b/snapshot/android/snapshot-generator-tools/mksnapshot/mksnapshot-5.5.372.32/darwin-x64/mksnapshot-ia32 new file mode 100755 index 00000000..84c35a8a Binary files /dev/null and b/snapshot/android/snapshot-generator-tools/mksnapshot/mksnapshot-5.5.372.32/darwin-x64/mksnapshot-ia32 differ diff --git a/snapshot/android/snapshot-generator-tools/mksnapshot/mksnapshot-5.5.372.32/darwin-x64/mksnapshot-x64 b/snapshot/android/snapshot-generator-tools/mksnapshot/mksnapshot-5.5.372.32/darwin-x64/mksnapshot-x64 new file mode 100755 index 00000000..1f38b95e Binary files /dev/null and b/snapshot/android/snapshot-generator-tools/mksnapshot/mksnapshot-5.5.372.32/darwin-x64/mksnapshot-x64 differ diff --git a/snapshot/android/snapshot-generator-tools/mksnapshot/mksnapshot-5.5.372.32/linux-x64/mksnapshot-arm b/snapshot/android/snapshot-generator-tools/mksnapshot/mksnapshot-5.5.372.32/linux-x64/mksnapshot-arm new file mode 100755 index 00000000..8fe48192 Binary files /dev/null and b/snapshot/android/snapshot-generator-tools/mksnapshot/mksnapshot-5.5.372.32/linux-x64/mksnapshot-arm differ diff --git a/snapshot/android/snapshot-generator-tools/mksnapshot/mksnapshot-5.5.372.32/linux-x64/mksnapshot-arm64 b/snapshot/android/snapshot-generator-tools/mksnapshot/mksnapshot-5.5.372.32/linux-x64/mksnapshot-arm64 new file mode 100755 index 00000000..7a39c147 Binary files /dev/null and b/snapshot/android/snapshot-generator-tools/mksnapshot/mksnapshot-5.5.372.32/linux-x64/mksnapshot-arm64 differ diff --git a/snapshot/android/snapshot-generator-tools/mksnapshot/mksnapshot-5.5.372.32/linux-x64/mksnapshot-ia32 b/snapshot/android/snapshot-generator-tools/mksnapshot/mksnapshot-5.5.372.32/linux-x64/mksnapshot-ia32 new file mode 100755 index 00000000..743e3899 Binary files /dev/null and b/snapshot/android/snapshot-generator-tools/mksnapshot/mksnapshot-5.5.372.32/linux-x64/mksnapshot-ia32 differ diff --git a/snapshot/android/snapshot-generator-tools/ndk-build/.gitignore b/snapshot/android/snapshot-generator-tools/ndk-build/.gitignore new file mode 100644 index 00000000..3012f159 --- /dev/null +++ b/snapshot/android/snapshot-generator-tools/ndk-build/.gitignore @@ -0,0 +1,4 @@ +/libs +/obj +/jni/*/TNSSnapshot.c + diff --git a/snapshot/android/snapshot-generator-tools/ndk-build/jni/Android.mk b/snapshot/android/snapshot-generator-tools/ndk-build/jni/Android.mk new file mode 100644 index 00000000..caee9260 --- /dev/null +++ b/snapshot/android/snapshot-generator-tools/ndk-build/jni/Android.mk @@ -0,0 +1,7 @@ +LOCAL_PATH := $(call my-dir) + +include $(CLEAR_VARS) +LOCAL_MODULE := snapshot +LOCAL_SRC_FILES := $(TARGET_ARCH_ABI)/TNSSnapshot.c +include $(BUILD_SHARED_LIBRARY) + diff --git a/snapshot/android/snapshot-generator.js b/snapshot/android/snapshot-generator.js new file mode 100644 index 00000000..10d0d69a --- /dev/null +++ b/snapshot/android/snapshot-generator.js @@ -0,0 +1,135 @@ +const fs = require("fs"); +const shelljs = require("shelljs"); +const path = require("path"); +const os = require("os"); +const child_process = require("child_process"); + +const shellJsExecuteInDir = function(dir, action) { + var currentDir = shelljs.pwd(); + shelljs.cd(dir); + try { + action(); + } finally { + shelljs.cd(currentDir); + } +} + +function SnapshotGenerator(options) { + this.buildPath = options.buildPath || path.join(__dirname, "build"); +} +module.exports = SnapshotGenerator; + +SnapshotGenerator.MKSNAPSHOT_TOOLS_PATH = path.join(__dirname, "snapshot-generator-tools/mksnapshot"); +SnapshotGenerator.NDK_BUILD_SEED_PATH = path.join(__dirname, "snapshot-generator-tools/ndk-build"); +SnapshotGenerator.BUNDLE_PREAMBLE_PATH = path.join(__dirname, "snapshot-generator-tools/bundle-preamble.js"); +SnapshotGenerator.INCLUDE_GRADLE_PATH = path.join(__dirname, "snapshot-generator-tools/include.gradle"); +SnapshotGenerator.SNAPSHOT_PACKAGE_NANE = "nativescript-android-snapshot"; + +SnapshotGenerator.prototype.preprocessInputFile = function(inputFile, outputFile) { + // Make some modifcations on the original bundle and save it on the specified path + var bundlePreambleContent = fs.readFileSync(SnapshotGenerator.BUNDLE_PREAMBLE_PATH, "utf8"); + var snapshotFileContent = bundlePreambleContent + fs.readFileSync(inputFile, "utf8"); + fs.writeFileSync(outputFile, snapshotFileContent, { encoding: "utf8" }); +} + +SnapshotGenerator.prototype.getMksnapshotToolsDirOrThrow = function(v8Version) { + var hostOS = os.type().toLowerCase(); + hostOS = /^darwin/.test(hostOS) ? "darwin" : (/^linux/.test(hostOS) ? "linux" : (/^win/.test(hostOS) ? "win" : hostOS)); + var mksnapshotToolsDir = path.join(SnapshotGenerator.MKSNAPSHOT_TOOLS_PATH, "mksnapshot-" + v8Version, hostOS + "-" + os.arch()); + if (!fs.existsSync(mksnapshotToolsDir)) { + throw new Error("No snapshot tools available for v8 v" + v8Version + " on " + os.type() + " OS."); + } + return mksnapshotToolsDir; +} + +SnapshotGenerator.prototype.convertToAndroidArchName = function(archName) { + switch (archName) { + case "arm": return "armeabi-v7a"; + case "arm64": return "arm64-v8a"; + case "ia32": return "x86"; + case "x64": return "x64"; + default: return archName; + } +} + +SnapshotGenerator.prototype.runMksnapshotTool = function(inputFile, v8Version, targetArchs, buildCSource) { + // Cleans the snapshot build folder + shelljs.rm("-rf", path.join(this.buildPath, "snapshots")); + + const mksnapshotToolsDir = this.getMksnapshotToolsDirOrThrow(v8Version); + const mksnapshotStdErrPath = path.join(this.buildPath, "mksnapshot-stderr.txt"); + for (var index in targetArchs) { + var arch = targetArchs[index]; + var currentArchMksnapshotToolPath = path.join(mksnapshotToolsDir, "mksnapshot-" + arch); + if (!fs.existsSync(currentArchMksnapshotToolPath)) { + console.log("***** Skipping " + arch + ". Unable to find mksnapshot tool for " + arch + ". *****"); + continue; + } + + var androidArch = this.convertToAndroidArchName(arch); + console.log("***** Generating snapshot for " + androidArch + " *****"); + + // Generate .blob file + var currentArchBlobOutputPath = path.join(this.buildPath, "snapshots/blobs", androidArch); + shelljs.mkdir("-p", currentArchBlobOutputPath); + var stdErrorStream = fs.openSync(mksnapshotStdErrPath, 'w'); + child_process.execSync(currentArchMksnapshotToolPath + " " + inputFile + " --startup_blob " + path.join(currentArchBlobOutputPath, "TNSSnapshot.blob") + " --profile_deserialization", {encoding: "utf8", stdio: [process.stdin, process.stdout, stdErrorStream]}); + fs.closeSync(stdErrorStream); + + if (fs.statSync(mksnapshotStdErrPath).size) { + console.error("***** SNAPSHOT GENERATION FOR " + androidArch + " FAILED! *****"); + var errorMessage = fs.readFileSync(mksnapshotStdErrPath, "utf8"); + // console.error(errorMessage); + throw new Error(errorMessage); + } + + // Generate .c file + if (buildCSource) { + var currentArchSrcOutputPath = path.join(this.buildPath, "snapshots/src", androidArch); + shelljs.mkdir("-p", currentArchSrcOutputPath); + shellJsExecuteInDir(currentArchBlobOutputPath, function(){ + shelljs.exec("xxd -i TNSSnapshot.blob > " + path.join(currentArchSrcOutputPath, "TNSSnapshot.c")); + }); + } + } + console.log("***** Finished generating snapshots. *****"); +} + +SnapshotGenerator.prototype.buildSnapshotLibs = function(androidNdkBuildPath, targetArchs) { + // Compile *.c files to produce *.so libraries with ndk-build tool + const ndkBuildPath = path.join(this.buildPath, "ndk-build"); + const androidArchs = targetArchs.map(arch => this.convertToAndroidArchName(arch)); + console.log("Building native libraries for " + androidArchs.join()); + shelljs.rm("-rf", ndkBuildPath); + shelljs.cp("-r", SnapshotGenerator.NDK_BUILD_SEED_PATH, ndkBuildPath); + fs.writeFileSync(path.join(ndkBuildPath, "jni/Application.mk"), "APP_ABI := " + androidArchs.join(" ")); // create Application.mk file + shelljs.mv(path.join(this.buildPath, "snapshots/src/*"), path.join(ndkBuildPath, "jni")); + shellJsExecuteInDir(ndkBuildPath, function(){ + shelljs.exec(androidNdkBuildPath); + }); + return path.join(ndkBuildPath, "libs"); +} + +SnapshotGenerator.prototype.buildIncludeGradle = function() { + shelljs.cp(SnapshotGenerator.INCLUDE_GRADLE_PATH, path.join(this.buildPath, "include.gradle")); +} + +SnapshotGenerator.prototype.generate = function(options) { + // Arguments validation + options = options || {}; + if (!options.inputFile) { throw new Error("inputFile option is not specified."); } + if (!shelljs.test("-e", options.inputFile)) { throw new Error("Can't find V8 snapshot input file: '" + options.inputFile + "'."); } + if (!options.targetArchs || options.targetArchs.length == 0) { throw new Error("No target archs specified."); } + if (!options.v8Version) { throw new Error("No v8 version specified."); } + var preprocessedInputFile = options.preprocessedInputFile || path.join(this.buildPath, "inputFile.preprocessed"); + + this.preprocessInputFile(options.inputFile, preprocessedInputFile); + this.runMksnapshotTool(preprocessedInputFile, options.v8Version, options.targetArchs, options.useLibs); // generates the actual .blob and .c files + + if (options.useLibs) { + const androidNdkBuildPath = options.androidNdkPath ? path.join(options.androidNdkPath, "ndk-build") : "ndk-build"; + this.buildSnapshotLibs(androidNdkBuildPath, options.targetArchs); + this.buildIncludeGradle(); + } + return this.buildPath; +} \ No newline at end of file diff --git a/snapshot/android/tns-java-classes-generator.js b/snapshot/android/tns-java-classes-generator.js new file mode 100644 index 00000000..6e835841 --- /dev/null +++ b/snapshot/android/tns-java-classes-generator.js @@ -0,0 +1,66 @@ +const fs = require("fs"); +const path = require("path"); +const shelljs = require("shelljs"); + +function TnsJavaClassesGenerator() {} +module.exports = TnsJavaClassesGenerator; + +TnsJavaClassesGenerator.prototype.generate = function(generationOptions) { + // Arguments validation + generationOptions = generationOptions || {}; + if (!generationOptions.projectRoot) { throw new Error("No projectRoot specified."); } + var initialSettings = generationOptions.options || { modules: [], packages: [] }; + initialSettings.modules = initialSettings.modules || []; + initialSettings.packages = initialSettings.packages || []; + + const packageJsonPath = path.join(generationOptions.projectRoot, "package.json"); + const nodeModulesPath = path.join(generationOptions.projectRoot, "node_modules"); + + /* + "tns-java-classes": { + "modules": ["packageX/moduleX", "./app/moduleY"], + "packages": ["package1", "package2"] + } + */ + var tnsJavaClassesSettings = this.getTnsJavaClassesSettings(packageJsonPath); + Array.prototype.push.apply(initialSettings.modules, tnsJavaClassesSettings.modules); + Array.prototype.push.apply(initialSettings.packages, tnsJavaClassesSettings.packages); + + var nodeModules = fs.readdirSync(nodeModulesPath).filter((moduleName) => initialSettings.packages.indexOf(moduleName) >= 0); + for(var i = 0; i < nodeModules.length; i++) { + var moduleName = nodeModules[i]; + var modulePackageJsonPath = path.join(nodeModulesPath, moduleName, "package.json"); + var moduleTnsJavaClassesSettings = this.getTnsJavaClassesSettings(modulePackageJsonPath); + // Backward compatibilty with modules 3.0.1 and below + if (moduleName == "tns-core-modules" && moduleTnsJavaClassesSettings.modules.length == 0) { + moduleTnsJavaClassesSettings = { modules: ["ui/frame/activity", "ui/frame/fragment"] }; + } + Array.prototype.push.apply(initialSettings.modules, moduleTnsJavaClassesSettings.modules); + } + + // Generate the file + var tnsJavaClassesFileContent = initialSettings.modules.map(moduleName => "require(\"" + moduleName + "\");").join("\n"); + if (generationOptions.output) { + shelljs.mkdir("-p", path.dirname(generationOptions.output)); + if (generationOptions.outputAppend) { + var currentFileContent = shelljs.test("-e", generationOptions.output) ? fs.readFileSync(generationOptions.output, "utf8") : ""; + tnsJavaClassesFileContent = currentFileContent + tnsJavaClassesFileContent; + } + fs.writeFileSync(generationOptions.output, tnsJavaClassesFileContent, { encoding: "utf8" }); + } + return tnsJavaClassesFileContent; +} + +TnsJavaClassesGenerator.prototype.getTnsJavaClassesSettings = function(packageJsonPath) { + var packageJson = shelljs.test("-e", packageJsonPath) ? JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')) : {}; + if (packageJson.snapshot && + packageJson.snapshot.android && + packageJson.snapshot.android["tns-java-classes"]) { + var extendedJavaClasses = packageJson.snapshot.android["tns-java-classes"]; + extendedJavaClasses.modules = extendedJavaClasses.modules || []; + extendedJavaClasses.packages = extendedJavaClasses.packages || []; + return extendedJavaClasses; + } + + return { modules: [], packages: [] }; +} \ No newline at end of file diff --git a/templates/vendor-platform.android.ts b/templates/vendor-platform.android.ts index 6fb10f0c..ff64887d 100644 --- a/templates/vendor-platform.android.ts +++ b/templates/vendor-platform.android.ts @@ -1,3 +1,9 @@ require("application"); -require("ui/frame"); -require("ui/frame/activity"); +if (!global["__snapshot"]) { + /* + In case snapshot generation is enabled these modules will get into the bundle but will not be required/evaluated. + The snapshot webpack plugin will add them to the tns-java-classes.js bundle file. This way, they will be evaluated on app start as early as possible. + */ + require("ui/frame"); + require("ui/frame/activity"); +} diff --git a/templates/webpack.angular.js b/templates/webpack.angular.js index ce879f46..6c4676bc 100644 --- a/templates/webpack.angular.js +++ b/templates/webpack.angular.js @@ -31,7 +31,7 @@ module.exports = env => { const plugins = getPlugins(platform, env); const extensions = getExtensions(platform); - return { + const config = { context: resolve("./app"), target: nativescriptTarget, entry, @@ -60,6 +60,19 @@ module.exports = env => { module: { rules }, plugins, }; + + if (env.snapshot) { + plugins.push(new nsWebpack.NativeScriptSnapshotPlugin({ + chunk: "vendor", + projectRoot: __dirname, + webpackConfig: config, + targetArchs: ["arm", "arm64"], + tnsJavaClassesOptions: { packages: ["tns-core-modules" ] }, + useLibs: false + })); + } + + return config; }; @@ -168,7 +181,7 @@ function getPlugins(platform, env) { }), ]; - + if (env.uglify) { plugins.push(new webpack.LoaderOptionsPlugin({ minimize: true })); diff --git a/templates/webpack.javascript.js b/templates/webpack.javascript.js index daf8723f..9a24c060 100644 --- a/templates/webpack.javascript.js +++ b/templates/webpack.javascript.js @@ -30,7 +30,7 @@ module.exports = env => { const plugins = getPlugins(platform, env); const extensions = getExtensions(platform); - return { + const config = { context: resolve("./app"), target: nativescriptTarget, entry, @@ -59,6 +59,19 @@ module.exports = env => { module: { rules }, plugins, }; + + if (env.snapshot) { + plugins.push(new nsWebpack.NativeScriptSnapshotPlugin({ + chunk: "vendor", + projectRoot: __dirname, + webpackConfig: config, + targetArchs: ["arm", "arm64"], + tnsJavaClassesOptions: { packages: ["tns-core-modules" ] }, + useLibs: false + })); + } + + return config; }; @@ -142,7 +155,7 @@ function getPlugins(platform, env) { "./bundle", ]), ]; - + if (env.uglify) { plugins.push(new webpack.LoaderOptionsPlugin({ minimize: true })); diff --git a/templates/webpack.typescript.js b/templates/webpack.typescript.js index 20c642ca..a369855f 100644 --- a/templates/webpack.typescript.js +++ b/templates/webpack.typescript.js @@ -30,7 +30,7 @@ module.exports = env => { const plugins = getPlugins(platform, env); const extensions = getExtensions(platform); - return { + const config = { context: resolve("./app"), target: nativescriptTarget, entry, @@ -59,6 +59,19 @@ module.exports = env => { module: { rules }, plugins, }; + + if (env.snapshot) { + plugins.push(new nsWebpack.NativeScriptSnapshotPlugin({ + chunk: "vendor", + projectRoot: __dirname, + webpackConfig: config, + targetArchs: ["arm", "arm64"], + tnsJavaClassesOptions: { packages: ["tns-core-modules" ] }, + useLibs: false + })); + } + + return config; }; @@ -151,7 +164,7 @@ function getPlugins(platform, env) { "./bundle", ]), ]; - + if (env.uglify) { plugins.push(new webpack.LoaderOptionsPlugin({ minimize: true })); diff --git a/utils.js b/utils.js new file mode 100644 index 00000000..d6d7a85d --- /dev/null +++ b/utils.js @@ -0,0 +1,34 @@ +const VERSION_MATCHER = /(\d+)\.(\d+)\.(\d+)/; +const parseVersion = version => { + const semver = VERSION_MATCHER.exec(version); + + return semver && semver.length > 3 && semver.splice(1).map(Number); +}; + +const isVersionValid = version => !!parseVersion(version); + +const isVersionGte = (first, second) => { + if (!isVersionValid(first) || !isVersionValid(second)) { + return false; + } + + const [ fMajor, fMinor, fPatch ] = parseVersion(first); + const [ sMajor, sMinor, sPatch ] = parseVersion(second); + + return ( + fMajor - sMajor || + fMinor - sMinor || + fPatch - sPatch + ) >= 0; +} + +const sanitize = name => name + .split("") + .filter(char => /[a-zA-Z0-9]/.test(char)) + .join(""); + +module.exports = { + isVersionGte, + sanitize, +}; +