'use strict'; // Canonical path provides a consistent path (i.e. always forward slashes) across different OSes const path = require('canonical-path'); const fs = require('fs-extra'); const globby = require('globby'); const jsdom = require('jsdom'); const json5 = require('json5'); const regionExtractor = require('../transforms/examples-package/services/region-parser'); class StackblitzBuilder { constructor(basePath, destPath) { this.basePath = basePath; this.destPath = destPath; this.copyrights = this._buildCopyrightStrings(); this._boilerplatePackageJsons = {}; } build() { this._checkForOutdatedConfig(); // When testing it sometimes helps to look a just one example directory like so: // const stackblitzPaths = path.join(this.basePath, '**/testing/*stackblitz.json'); const stackblitzPaths = path.join(this.basePath, '**/*stackblitz.json'); const fileNames = globby.sync(stackblitzPaths, { ignore: ['**/node_modules/**'] }); let failed = false; fileNames.forEach((configFileName) => { try { this._buildStackblitzFrom(configFileName); } catch (e) { failed = true; console.log(e); } }); if (failed) { process.exit(1); } } _addDependencies(config, postData) { // Extract npm package dependencies const exampleType = this._getExampleType(config.basePath); const packageJson = this._getBoilerplatePackageJson(exampleType) || this._getBoilerplatePackageJson('cli'); const exampleDependencies = packageJson.dependencies; // Add unit test packages from devDependencies for unit test examples const devDependencies = packageJson.devDependencies; (config.devDependencies || []).forEach(dep => exampleDependencies[dep] = devDependencies[dep]); postData.dependencies = JSON.stringify(exampleDependencies); } _getExampleType(exampleDir) { const configPath = `${exampleDir}/example-config.json`; const configSrc = fs.existsSync(configPath) && fs.readFileSync(configPath, 'utf-8').trim(); const config = configSrc ? JSON.parse(configSrc) : {}; return config.projectType || 'cli'; } _getBoilerplatePackageJson(exampleType) { if (!this._boilerplatePackageJsons.hasOwnProperty(exampleType)) { const pkgJsonPath = `${__dirname}/../examples/shared/boilerplate/${exampleType}/package.json`; this._boilerplatePackageJsons[exampleType] = fs.existsSync(pkgJsonPath) ? require(pkgJsonPath) : null; } return this._boilerplatePackageJsons[exampleType]; } _buildCopyrightStrings() { const copyright = 'Copyright Google LLC. All Rights Reserved.\n' + 'Use of this source code is governed by an MIT-style license that\n' + 'can be found in the LICENSE file at https://angular.io/license'; const pad = '\n\n'; return { jsCss: `${pad}/*\n${copyright}\n*/`, html: `${pad}`, }; } // Build stackblitz from JSON configuration file (e.g., stackblitz.json): // all properties are optional // files: string[] - array of globs - defaults to all js, ts, html, json, css and md files (with certain files removed) // description: string - description of this stackblitz - defaults to the title in the index.html page. // tags: string[] - optional array of stackblitz tags (for searchability) // main: string - name of file that will become index.html in the stackblitz - defaults to index.html // file: string - name of file to display within the stackblitz (e.g. `"file": "app/app.module.ts"`) _buildStackblitzFrom(configFileName) { // replace ending 'stackblitz.json' with 'stackblitz.no-link.html' to create output file name; const outputFileName = configFileName.replace(/stackblitz\.json$/, 'stackblitz.no-link.html'); let altFileName; if (this.destPath && this.destPath.length > 0) { const partPath = path.dirname(path.relative(this.basePath, outputFileName)); altFileName = path.join(this.destPath, partPath, path.basename(outputFileName)).replace('.no-link.', '.'); } try { const config = this._initConfigAndCollectFileNames(configFileName); const postData = this._createPostData(config, configFileName); this._addDependencies(config, postData); const html = this._createStackblitzHtml(config, postData); fs.writeFileSync(outputFileName, html, 'utf-8'); if (altFileName) { const altDirName = path.dirname(altFileName); fs.ensureDirSync(altDirName); fs.writeFileSync(altFileName, html, 'utf-8'); } } catch (e) { // if we fail delete the outputFile if it exists because it is an old one. if (fs.existsSync(outputFileName)) { fs.unlinkSync(outputFileName); } if (altFileName && fs.existsSync(altFileName)) { fs.unlinkSync(altFileName); } throw e; } } _checkForOutdatedConfig() { // Ensure that nobody is trying to use the old config filenames (i.e. `plnkr.json`). const plunkerPaths = path.join(this.basePath, '**/*plnkr.json'); const fileNames = globby.sync(plunkerPaths, { ignore: ['**/node_modules/**'] }); if (fileNames.length) { const readmePath = path.join(__dirname, 'README.md'); const errorMessage = 'One or more examples are still trying to use \'plnkr.json\' files for configuring ' + 'live examples. This is not supported any more. \'stackblitz.json\' should be used ' + 'instead.\n' + `(Slight modifications may be required. See '${readmePath}' for more info.\n\n` + fileNames.map(name => `- ${name}`).join('\n'); throw Error(errorMessage); } } _getPrimaryFile(config) { if (config.file) { if (!fs.existsSync(path.join(config.basePath, config.file))) { throw new Error(`The specified primary file (${config.file}) does not exist in '${config.basePath}'.`); } return config.file; } else { const defaultPrimaryFiles = ['src/app/app.component.html', 'src/app/app.component.ts', 'src/app/main.ts']; const primaryFile = defaultPrimaryFiles.find(fileName => fs.existsSync(path.join(config.basePath, fileName))); if (!primaryFile) { throw new Error(`None of the default primary files (${defaultPrimaryFiles.join(', ')}) exists in '${config.basePath}'.`); } return primaryFile; } } _createBaseStackblitzHtml(config) { const file = `?file=${this._getPrimaryFile(config)}`; const action = `https://run.stackblitz.com/api/angular/v1${file}`; return `
`.trim(); } _createPostData(config, configFileName) { const postData = {}; // If `config.main` is specified, ensure that it points to an existing file. if (config.main && !fs.existsSync(path.join(config.basePath, config.main))) { throw Error(`The main file ('${config.main}') specified in '${configFileName}' does not exist.`); } config.fileNames.forEach((fileName) => { let content; const extn = path.extname(fileName); if (extn === '.png') { content = this._encodeBase64(fileName); fileName = `${fileName.slice(0, -extn.length)}.base64${extn}`; } else { content = fs.readFileSync(fileName, 'utf-8'); } if (extn === '.js' || extn === '.ts' || extn === '.css') { content = content + this.copyrights.jsCss; } else if (extn === '.html') { content = content + this.copyrights.html; } // const escapedValue = escapeHtml(content); let relativeFileName = path.relative(config.basePath, fileName); // Is the main a custom index-xxx.html file? Rename it if (relativeFileName === config.main) { relativeFileName = 'src/index.html'; } // A custom main.ts file? Rename it if (/src\/main[-.]\w+\.ts$/.test(relativeFileName)) { relativeFileName = 'src/main.ts'; } if (relativeFileName === 'index.html') { if (config.description == null) { // set config.description to title from index.html const matches = /