diff --git a/aio/BUILD.bazel b/aio/BUILD.bazel index 70c70c6fd9e..48b32e5df38 100644 --- a/aio/BUILD.bazel +++ b/aio/BUILD.bazel @@ -1,5 +1,6 @@ load("@aio_npm//@angular-devkit/architect-cli:index.bzl", "architect", "architect_test") -load("//tools:defaults.bzl", "npm_package_bin") +load("@build_bazel_rules_nodejs//:index.bzl", "npm_package_bin") +load("@aspect_bazel_lib//lib:copy_to_directory.bzl", "copy_to_directory") # The write_source_files macro is used to write bazel outputs to the source tree and test that they are up to date. # See: https://docs.aspect.build/aspect-build/bazel-lib/v0.5.0/docs/docs/write_source_files-docgen.html @@ -26,6 +27,101 @@ generated_files_test( }, ) +# External deps required by dgeni transforms +DGENI_DEPS = [ + "@aio_npm//css-selector-parser", + "@aio_npm//dgeni-packages", + "@aio_npm//entities", + "@aio_npm//fs-extra", + "@aio_npm//hast-util-is-element", + "@aio_npm//hast-util-to-string", + "@aio_npm//ignore", + "@aio_npm//image-size", + "@aio_npm//json5", + "@aio_npm//rehype-slug", + "@aio_npm//remark", + "@aio_npm//remark-html", + "@aio_npm//rxjs", + "@aio_npm//semver", + "@aio_npm//stemmer", + "@aio_npm//unist-util-filter", + "@aio_npm//unist-util-source", +] + +# All transforms and sources needed by dgeni to produce docs +DGENI_FILES = [ + "@angular_cli_src//:files_for_docgen", # Downloaded cli snapshot (see WORKSPACE) + "//aio/content", + "//aio/content/examples", + "//aio/src/assets", + "//aio/tools/transforms", + "//:package.json", + "//packages:files_for_docgen", +] + +# Run dgeni generation +npm_package_bin( + name = "dgeni", + args = ["./aio/tools/transforms/angular.io-package"], + configuration_env_vars = ["PATH"], # Need PATH for dgeni package that invokes git + data = DGENI_FILES + DGENI_DEPS, + env = { + "BAZEL_DGENI_OUTPUT_PATH": "$(@D)", + }, + output_dir = True, + tool = "@aio_npm//dgeni/bin:dgeni", +) + +# Generate example boilerplate files +npm_package_bin( + name = "example-boilerplate", + args = ["add"], + env = { + "BAZEL_EXAMPLE_BOILERPLATE_OUTPUT_PATH": "$(@D)", + }, + output_dir = True, + tool = "//aio/tools/examples:example-boilerplate", +) + +# Combine example source files with generated boilerplate files +copy_to_directory( + name = "examples", + # Prevent sorting so that boilerplate overwrites example sources + # buildifier: do not sort + srcs = [ + "//aio/content/examples", + ":example-boilerplate", + ], + replace_prefixes = { + "content/examples": "", + "example-boilerplate": "", + }, +) + +# Generate stackblitz live examples +npm_package_bin( + name = "stackblitz", + args = [ + "$(RULEDIR)/examples", + "$(@D)", + ], + data = [":examples"], + output_dir = True, + tool = "//aio/tools/stackblitz-builder:generate-stackblitz", +) + +# Generate example zips +npm_package_bin( + name = "example-zips", + args = [ + "$(RULEDIR)/examples", + "$(@D)", + ], + data = [":examples"], + output_dir = True, + tool = "//aio/tools/example-zipper:generate-example-zips", +) + # All source and configuration files required to build the docs app APPLICATION_FILES = [ "angular.json", @@ -37,6 +133,8 @@ APPLICATION_FILES = [ "//aio/src/assets", "//aio/src/assets/js", ":dgeni", + ":stackblitz", + ":example-zips", ] + glob( ["src/**/*"], exclude = [ @@ -118,50 +216,6 @@ architect_test( data = TEST_FILES + TEST_DEPS, ) -# External deps required by dgeni transforms -DGENI_DEPS = [ - "@aio_npm//css-selector-parser", - "@aio_npm//dgeni-packages", - "@aio_npm//entities", - "@aio_npm//fs-extra", - "@aio_npm//hast-util-is-element", - "@aio_npm//hast-util-to-string", - "@aio_npm//ignore", - "@aio_npm//image-size", - "@aio_npm//json5", - "@aio_npm//rehype-slug", - "@aio_npm//remark", - "@aio_npm//remark-html", - "@aio_npm//rxjs", - "@aio_npm//semver", - "@aio_npm//stemmer", - "@aio_npm//unist-util-filter", - "@aio_npm//unist-util-source", -] - -# All transforms and sources needed by dgeni to produce docs -DGENI_FILES = [ - "@angular_cli_src//:files_for_docgen", # Downloaded cli snapshot (see WORKSPACE) - "//aio/content", - "//aio/src/assets", - "//aio/tools/transforms", - "//:package.json", - "//packages:files_for_docgen", -] - -# Run dgeni generation -npm_package_bin( - name = "dgeni", - args = ["./aio/tools/transforms/angular.io-package"], - configuration_env_vars = ["PATH"], # Need PATH for dgeni package that invokes git - data = DGENI_FILES + DGENI_DEPS, - env = { - "BAZEL_DGENI_OUTPUT_PATH": "$(@D)", - }, - output_dir = True, - tool = "@aio_npm//dgeni/bin:dgeni", -) - architect( name = "serve", args = [ diff --git a/aio/angular.json b/aio/angular.json index 56d0c179d9b..67daad4b5fa 100644 --- a/aio/angular.json +++ b/aio/angular.json @@ -54,6 +54,16 @@ "input": "dgeni/generated", "output": "generated", "glob": "**" + }, + { + "input": "stackblitz/generated", + "output": "generated", + "glob": "**" + }, + { + "input": "example-zips/generated", + "output": "generated", + "glob": "**" } ], "styles": [ diff --git a/aio/content/examples/BUILD.bazel b/aio/content/examples/BUILD.bazel new file mode 100644 index 00000000000..4baa235af04 --- /dev/null +++ b/aio/content/examples/BUILD.bazel @@ -0,0 +1,6 @@ +package(default_visibility = ["//visibility:public"]) + +filegroup( + name = "examples", + srcs = glob(["**"]), +) diff --git a/aio/tools/example-zipper/BUILD.bazel b/aio/tools/example-zipper/BUILD.bazel new file mode 100644 index 00000000000..35a3fe0be1e --- /dev/null +++ b/aio/tools/example-zipper/BUILD.bazel @@ -0,0 +1,19 @@ +load("@build_bazel_rules_nodejs//:index.bzl", "nodejs_binary") + +package(default_visibility = ["//visibility:public"]) + +nodejs_binary( + name = "generate-example-zips", + data = [ + "exampleZipper.mjs", + "//aio/tools/transforms", + "@aio_npm//archiver", + "@aio_npm//canonical-path", + "@aio_npm//fs-extra", + "@aio_npm//globby", + ], + entry_point = "generateZips.mjs", + # --preserve-symlinks-main is not enabled by default (see https://github.com/bazelbuild/rules_nodejs/pull/2176) + # However it seems to be required for mjs entry points to resolve node_modules within the sandbox. + templated_args = ["--node_options=--preserve-symlinks-main"], +) diff --git a/aio/tools/example-zipper/exampleZipper.mjs b/aio/tools/example-zipper/exampleZipper.mjs index f780dd1b1f1..c9a379cdbc5 100644 --- a/aio/tools/example-zipper/exampleZipper.mjs +++ b/aio/tools/example-zipper/exampleZipper.mjs @@ -19,7 +19,11 @@ export class ExampleZipper { let gpathStackblitz = path.join(sourceDirName, '**/*stackblitz.json'); let gpathZipper = path.join(sourceDirName, '**/zipper.json'); - let configFileNames = globbySync([gpathStackblitz, gpathZipper], { ignore: ['**/node_modules/**'] }); + let configFileNames = globbySync([gpathStackblitz, gpathZipper], { + ignore: ['**/node_modules/**'], + dot: true // Include subpaths that begin with '.' when using a wildcard inclusion. + // Needed to include the bazel .cache folder on Linux. + }); configFileNames.forEach((configFileName) => { this._zipExample(configFileName, sourceDirName, outputDirName); }); @@ -152,7 +156,11 @@ export class ExampleZipper { gpaths.push(...alwaysExcludes); - let fileNames = globbySync(gpaths, { ignore: ['**/node_modules/**'] }); + let fileNames = globbySync(gpaths, { + ignore: ['**/node_modules/**'], + dot: true // Include subpaths that begin with '.' when using a wildcard inclusion. + // Needed to include the bazel .cache folder on Linux. + }); let zip = this._createZipArchive(outputFileName); fileNames.forEach((fileName) => { diff --git a/aio/tools/example-zipper/generateZips.mjs b/aio/tools/example-zipper/generateZips.mjs index 3ae43b83cf9..4d959579009 100644 --- a/aio/tools/example-zipper/generateZips.mjs +++ b/aio/tools/example-zipper/generateZips.mjs @@ -1,9 +1,14 @@ import path from 'canonical-path'; -import {fileURLToPath} from 'url'; import {ExampleZipper} from './exampleZipper.mjs'; -const __dirname = path.dirname(fileURLToPath(import.meta.url)); -const EXAMPLES_PATH = path.join(__dirname, '../../content/examples'); -const ZIPS_PATH = path.join(__dirname, '../../src/generated/zips'); +const argv = process.argv.slice(2); +if (argv.length !== 2) { + console.error("Usage: node generateZips.mjs [examples-path] [output-path]"); + process.exit(1); +} + +const EXAMPLES_PATH = argv[0]; +const OUTPUT_PATH = argv[1]; +const ZIPS_PATH = path.join(OUTPUT_PATH, 'generated', 'zips'); new ExampleZipper(EXAMPLES_PATH, ZIPS_PATH); diff --git a/aio/tools/examples/BUILD.bazel b/aio/tools/examples/BUILD.bazel new file mode 100644 index 00000000000..17929e9d3cb --- /dev/null +++ b/aio/tools/examples/BUILD.bazel @@ -0,0 +1,17 @@ +load("@build_bazel_rules_nodejs//:index.bzl", "nodejs_binary") + +package(default_visibility = ["//visibility:public"]) + +nodejs_binary( + name = "example-boilerplate", + data = [ + "constants.js", + "//aio/content/examples", + "@aio_npm//canonical-path", + "@aio_npm//fs-extra", + "@aio_npm//ignore", + "@aio_npm//shelljs", + "@aio_npm//yargs", + ] + glob(["shared/**"]), + entry_point = "example-boilerplate.js", +) diff --git a/aio/tools/examples/constants.js b/aio/tools/examples/constants.js index c7d590d9d93..afdc489cfd4 100644 --- a/aio/tools/examples/constants.js +++ b/aio/tools/examples/constants.js @@ -4,3 +4,4 @@ exports.EXAMPLES_BASE_PATH = path.resolve(__dirname, '../../content/examples'); exports.EXAMPLE_CONFIG_FILENAME = 'example-config.json'; exports.SHARED_PATH = path.resolve(__dirname, 'shared'); exports.STACKBLITZ_CONFIG_FILENAME = 'stackblitz.json'; +exports.BAZEL_EXAMPLE_BOILERPLATE_OUTPUT_PATH = process.env.BAZEL_EXAMPLE_BOILERPLATE_OUTPUT_PATH; diff --git a/aio/tools/examples/example-boilerplate.js b/aio/tools/examples/example-boilerplate.js index 83b89d9efee..1716ccf5369 100644 --- a/aio/tools/examples/example-boilerplate.js +++ b/aio/tools/examples/example-boilerplate.js @@ -4,7 +4,7 @@ const ignore = require('ignore'); const path = require('canonical-path'); const shelljs = require('shelljs'); const yargs = require('yargs'); -const {EXAMPLES_BASE_PATH, EXAMPLE_CONFIG_FILENAME, SHARED_PATH} = require('./constants'); +const {EXAMPLES_BASE_PATH, BAZEL_EXAMPLE_BOILERPLATE_OUTPUT_PATH, EXAMPLE_CONFIG_FILENAME, SHARED_PATH} = require('./constants'); const SHARED_NODE_MODULES_PATH = path.resolve(SHARED_PATH, 'node_modules'); @@ -22,13 +22,16 @@ class ExampleBoilerPlate { this.getFoldersContaining(EXAMPLES_BASE_PATH, EXAMPLE_CONFIG_FILENAME, 'node_modules'); const gitignore = ignore().add(fs.readFileSync(path.resolve(BOILERPLATE_BASE_PATH, '.gitignore'), 'utf8')); - if (!fs.existsSync(SHARED_NODE_MODULES_PATH)) { - throw new Error( - `The shared node_modules folder for the examples (${SHARED_NODE_MODULES_PATH}) is missing.\n` + - 'Perhaps you need to run "yarn example-use-npm" or "yarn example-use-local" to install the dependencies?'); - } - - shelljs.exec(`yarn --cwd ${SHARED_PATH} ngcc --properties es2015 main`); + // TODO(bazel): Comment this out until example tests are run with bazel. The node_modules folder isn't + // needed to build the examples. + // + // if (!fs.existsSync(SHARED_NODE_MODULES_PATH)) { + // throw new Error( + // `The shared node_modules folder for the examples (${SHARED_NODE_MODULES_PATH}) is missing.\n` + + // 'Perhaps you need to run "yarn example-use-npm" or "yarn example-use-local" to install the dependencies?'); + // } + // + // shelljs.exec(`yarn --cwd ${SHARED_PATH} ngcc --properties es2015 main`); exampleFolders.forEach(exampleFolder => { const exampleConfig = this.loadJsonFile(path.resolve(exampleFolder, EXAMPLE_CONFIG_FILENAME)); @@ -43,26 +46,31 @@ class ExampleBoilerPlate { ); const isPathIgnored = absolutePath => boilerplateIgnore.ignores(path.relative(BOILERPLATE_BASE_PATH, absolutePath)); + // TODO(bazel): Comment this out until example tests are being run with bazel. The node_modules folder isn't + // needed to build the examples. + // // Link the node modules - requires admin access (on Windows) because it adds symlinks - const destinationNodeModules = path.resolve(exampleFolder, 'node_modules'); - fs.ensureSymlinkSync(SHARED_NODE_MODULES_PATH, destinationNodeModules); + // const destinationNodeModules = path.resolve(exampleFolder, 'node_modules'); + // fs.ensureSymlinkSync(SHARED_NODE_MODULES_PATH, destinationNodeModules); const boilerPlateType = exampleConfig.projectType || 'cli'; const boilerPlateBasePath = path.resolve(BOILERPLATE_BASE_PATH, boilerPlateType); + const outputPath = path.join(BAZEL_EXAMPLE_BOILERPLATE_OUTPUT_PATH, path.basename(exampleFolder)); + shelljs.mkdir('-p', outputPath); // All example types other than `cli` and `systemjs` are based on `cli`. Copy over the `cli` // boilerplate files first. // (Some of these files might be later overwritten by type-specific files.) if (boilerPlateType !== 'cli' && boilerPlateType !== 'systemjs') { - this.copyDirectoryContents(BOILERPLATE_CLI_PATH, exampleFolder, isPathIgnored); + this.copyDirectoryContents(BOILERPLATE_CLI_PATH, outputPath, isPathIgnored); } // Copy the type-specific boilerplate files. - this.copyDirectoryContents(boilerPlateBasePath, exampleFolder, isPathIgnored); + this.copyDirectoryContents(boilerPlateBasePath, outputPath, isPathIgnored); // Copy the common boilerplate files (unless explicitly not used). if (exampleConfig.useCommonBoilerplate !== false) { - this.copyDirectoryContents(BOILERPLATE_COMMON_PATH, exampleFolder, isPathIgnored); + this.copyDirectoryContents(BOILERPLATE_COMMON_PATH, outputPath, isPathIgnored); } }); } diff --git a/aio/tools/stackblitz-builder/BUILD.bazel b/aio/tools/stackblitz-builder/BUILD.bazel new file mode 100644 index 00000000000..d593faaef60 --- /dev/null +++ b/aio/tools/stackblitz-builder/BUILD.bazel @@ -0,0 +1,19 @@ +load("@build_bazel_rules_nodejs//:index.bzl", "nodejs_binary") + +package(default_visibility = ["//visibility:public"]) + +nodejs_binary( + name = "generate-stackblitz", + data = [ + "builder.mjs", + "//aio/tools/transforms", + "@aio_npm//canonical-path", + "@aio_npm//fs-extra", + "@aio_npm//globby", + "@aio_npm//jsdom", + ], + entry_point = "generateStackblitz.mjs", + # --preserve-symlinks-main is not enabled by default (see https://github.com/bazelbuild/rules_nodejs/pull/2176) + # However it seems to be required for mjs entry points to resolve node_modules within the sandbox. + templated_args = ["--node_options=--preserve-symlinks-main"], +) diff --git a/aio/tools/stackblitz-builder/builder.mjs b/aio/tools/stackblitz-builder/builder.mjs index 3867bd3aa96..a2b12b5a1c1 100644 --- a/aio/tools/stackblitz-builder/builder.mjs +++ b/aio/tools/stackblitz-builder/builder.mjs @@ -3,17 +3,13 @@ import path from 'canonical-path'; import fs from 'fs-extra'; import {globbySync} from 'globby'; import jsdom from 'jsdom'; -import {fileURLToPath} from 'url'; import regionExtractor from '../transforms/examples-package/services/region-parser.js'; -const __dirname = path.dirname(fileURLToPath(import.meta.url)); - export class StackblitzBuilder { constructor(basePath, destPath) { this.basePath = basePath; this.destPath = destPath; - this.copyrights = this._buildCopyrightStrings(); this._boilerplatePackageJsons = {}; } @@ -24,7 +20,10 @@ export class StackblitzBuilder { // 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 = globbySync(stackblitzPaths, { ignore: ['**/node_modules/**'] }); + const fileNames = globbySync(stackblitzPaths, { ignore: ['**/node_modules/**'], + dot: true // Include subpaths that begin with '.' when using a wildcard inclusion. + // Needed to include the bazel .cache folder on Linux. + }); let failed = false; fileNames.forEach((configFileName) => { try { @@ -79,42 +78,28 @@ export class StackblitzBuilder { // 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); + // replace ending 'stackblitz.json' with 'stackblitz.html' to create output file name; + const outputFileName = configFileName.replace(/stackblitz\.json$/, 'stackblitz.html'); + const partPath = path.dirname(path.relative(this.basePath, outputFileName)); + const outputFilePath = path.join(this.destPath, partPath, path.basename(outputFileName)); + const config = this._initConfigAndCollectFileNames(configFileName); + const postData = this._createPostData(config, configFileName); - this._addStackblitzrc(postData); + this._addStackblitzrc(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; - } + const html = this._createStackblitzHtml(config, postData); + const outputDirName = path.dirname(outputFilePath); + fs.ensureDirSync(outputDirName); + fs.writeFileSync(outputFilePath, html, 'utf-8'); } _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 = globbySync(plunkerPaths, { ignore: ['**/node_modules/**'] }); + const fileNames = globbySync(plunkerPaths, { ignore: ['**/node_modules/**'], + dot: true // Include subpaths that begin with '.' when using a wildcard inclusion. + // Needed to include the bazel .cache folder on Linux. + }); if (fileNames.length) { const readmePath = path.join(__dirname, 'README.md'); @@ -309,7 +294,11 @@ export class StackblitzBuilder { gpaths.push(...defaultExcludes); - config.fileNames = globbySync(gpaths, { ignore: ['**/node_modules/**'] }); + config.fileNames = globbySync(gpaths, { + ignore: ['**/node_modules/**'], + dot: true // Include subpaths that begin with '.' when using a wildcard inclusion. + // Needed to include the bazel .cache folder on Linux. + }); return config; } diff --git a/aio/tools/stackblitz-builder/generateStackblitz.mjs b/aio/tools/stackblitz-builder/generateStackblitz.mjs index 8a8e91b0de5..b4da4bb963a 100644 --- a/aio/tools/stackblitz-builder/generateStackblitz.mjs +++ b/aio/tools/stackblitz-builder/generateStackblitz.mjs @@ -1,9 +1,14 @@ -import {dirname, join} from 'path'; -import {fileURLToPath} from 'url'; +import {join} from 'path'; import {StackblitzBuilder} from './builder.mjs'; -const __dirname = dirname(fileURLToPath(import.meta.url)); -const EXAMPLES_PATH = join(__dirname, '../../content/examples'); -const LIVE_EXAMPLES_PATH = join(__dirname, '../../src/generated/live-examples'); +const argv = process.argv.slice(2); +if (argv.length !== 2) { + console.error("Usage: node generateStackblitz.mjs [examples-path] [output-path]"); + process.exit(1); +} + +const EXAMPLES_PATH = argv[0]; +const OUTPUT_PATH = argv[1]; +const LIVE_EXAMPLES_PATH = join(OUTPUT_PATH, 'generated', 'live-examples'); new StackblitzBuilder(EXAMPLES_PATH, LIVE_EXAMPLES_PATH).build();