diff --git a/adev/BUILD.bazel b/adev/BUILD.bazel index dd76812db6f..1d806c486ba 100644 --- a/adev/BUILD.bazel +++ b/adev/BUILD.bazel @@ -40,6 +40,7 @@ APPLICATION_ASSETS = [ "//adev/src/assets/icons", "//adev/src/assets:api", "//adev/src/assets:content", + "//adev/src/content/examples/accessibility:example", ] APPLICATION_DEPS = [ diff --git a/adev/shared-docs/index.bzl b/adev/shared-docs/index.bzl index f910c2911a1..576ecc16285 100644 --- a/adev/shared-docs/index.bzl +++ b/adev/shared-docs/index.bzl @@ -1,11 +1,13 @@ load("//adev/shared-docs/pipeline:_guides.bzl", _generate_guides = "generate_guides") load("//adev/shared-docs/pipeline:_navigation.bzl", _generate_nav_items = "generate_nav_items") load("//adev/shared-docs/pipeline:_playground.bzl", _generate_playground = "generate_playground") +load("//adev/shared-docs/pipeline:_previews.bzl", _generate_previews = "generate_previews") load("//adev/shared-docs/pipeline:_stackblitz.bzl", _generate_stackblitz = "generate_stackblitz") load("//adev/shared-docs/pipeline:_tutorial.bzl", _generate_tutorial = "generate_tutorial") generate_guides = _generate_guides generate_stackblitz = _generate_stackblitz +generate_previews = _generate_previews generate_playground = _generate_playground generate_tutorial = _generate_tutorial generate_nav_items = _generate_nav_items diff --git a/adev/shared-docs/pipeline/BUILD.bazel b/adev/shared-docs/pipeline/BUILD.bazel index 2473df56771..3917ca7e35a 100644 --- a/adev/shared-docs/pipeline/BUILD.bazel +++ b/adev/shared-docs/pipeline/BUILD.bazel @@ -61,6 +61,22 @@ esbuild_esm_bundle( ], ) +esbuild_esm_bundle( + name = "previews-bundle", + entry_point = "//adev/shared-docs/pipeline/examples/previews:index.ts", + external = [ + "typescript", + ], + output = "previews.mjs", + platform = "node", + target = "es2022", + visibility = ["//visibility:public"], + deps = [ + "//adev/shared-docs/pipeline/examples/previews:index", + "@npm//typescript", + ], +) + esbuild_esm_bundle( name = "zip-bundle", entry_point = "//adev/shared-docs/pipeline/examples/zip:index.ts", @@ -112,6 +128,7 @@ esbuild_esm_bundle( exports_files([ "_guides.bzl", "_stackblitz.bzl", + "_previews.bzl", "_playground.bzl", "_tutorial.bzl", "_navigation.bzl", @@ -129,6 +146,15 @@ nodejs_binary( visibility = ["//visibility:public"], ) +nodejs_binary( + name = "previews", + data = [ + "@npm//typescript", + ], + entry_point = "//adev/shared-docs/pipeline:previews.mjs", + visibility = ["//visibility:public"], +) + nodejs_binary( name = "zip", entry_point = "//adev/shared-docs/pipeline:zip.mjs", diff --git a/adev/shared-docs/pipeline/_previews.bzl b/adev/shared-docs/pipeline/_previews.bzl new file mode 100644 index 00000000000..577858b16fc --- /dev/null +++ b/adev/shared-docs/pipeline/_previews.bzl @@ -0,0 +1,56 @@ +load("@build_bazel_rules_nodejs//:providers.bzl", "run_node") + +def _generate_previews(ctx): + """Implementation of the previews generator rule""" + + # File declaration of the generated ts file + ts_output = ctx.actions.declare_file("previews.ts") + + # Set the arguments for the actions inputs and out put location. + args = ctx.actions.args() + + # Path to the examples for which previews are being generated. + args.add(ctx.attr.example_srcs.label.package) + + # Path to the preview map template. + args.add(ctx.file.template_src) + + # Path to the ts output file to write to. + args.add(ts_output.path) + + ctx.runfiles(files = ctx.files.template_src) + + run_node( + ctx = ctx, + inputs = depset(ctx.files.example_srcs + ctx.files.template_src), + executable = "_generate_previews", + outputs = [ts_output], + arguments = [args], + ) + + # The return value describes what the rule is producing. In this case we need to specify + # the "DefaultInfo" with the output ts file. + return [DefaultInfo(files = depset([ts_output]))] + +generate_previews = rule( + # Point to the starlark function that will execute for this rule. + implementation = _generate_previews, + doc = """Rule that generates a map of example previews to their component""", + + # The attributes that can be set to this rule. + attrs = { + "example_srcs": attr.label( + doc = """Files used for the previews map generation.""", + ), + "template_src": attr.label( + doc = """The previews map template file to base the generated file on.""", + default = Label("//adev/shared-docs/pipeline/examples/previews:template"), + allow_single_file = True, + ), + "_generate_previews": attr.label( + default = Label("//adev/shared-docs/pipeline:previews"), + executable = True, + cfg = "exec", + ), + }, +) diff --git a/adev/shared-docs/pipeline/examples/previews/BUILD.bazel b/adev/shared-docs/pipeline/examples/previews/BUILD.bazel new file mode 100644 index 00000000000..8fa9377dd28 --- /dev/null +++ b/adev/shared-docs/pipeline/examples/previews/BUILD.bazel @@ -0,0 +1,28 @@ +load("//tools:defaults.bzl", "ts_library") + +package(default_visibility = ["//visibility:public"]) + +ts_library( + name = "index", + srcs = [ + "index.ts", + ], + visibility = [ + "//adev/shared-docs:__subpackages__", + ], + deps = [ + "@npm//@types/node", + "@npm//typescript", + ], +) + +filegroup( + name = "template", + srcs = ["previews.template"], + visibility = ["//visibility:public"], +) + +exports_files([ + "index.ts", + "previews.template", +]) diff --git a/adev/shared-docs/pipeline/examples/previews/index.ts b/adev/shared-docs/pipeline/examples/previews/index.ts new file mode 100644 index 00000000000..7a78bfdf3bf --- /dev/null +++ b/adev/shared-docs/pipeline/examples/previews/index.ts @@ -0,0 +1,149 @@ +/*! + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ + +import * as fs from 'fs'; +import {readFile, writeFile} from 'fs/promises'; +import {join, relative} from 'path'; +import ts from 'typescript'; + +const [examplesDir, templateFilePath, outputFilePath] = process.argv.slice(2); + +const TYPESCRIPT_EXTENSION = '.ts'; +const SKIP_FILES_WITH_EXTENSIONS = ['.e2e-spec.ts', '.spec.ts', '.po.ts']; +const EXAMPLES_PATH = `../../content/examples`; + +interface File { + path: string; + content: string; +} + +interface AnalyzedFiles { + path: string; + componentNames: string[]; +} + +main(); + +async function main() { + const files = await retrieveAllTypescriptFiles( + examplesDir, + (path) => !SKIP_FILES_WITH_EXTENSIONS.some((extensionToSkip) => path.endsWith(extensionToSkip)), + ); + + const filesWithComponent = files + .map((file) => ({ + componentNames: analyzeFile(file), + path: file.path, + })) + .filter((result) => result.componentNames.length > 0); + + const previewsComponentMap = generatePreviewsComponentMap(filesWithComponent); + + await writeFile(outputFilePath, previewsComponentMap); +} + +/** Recursively search the provided directory for all typescript files and asynchronously load them. */ +function retrieveAllTypescriptFiles( + baseDir: string, + predicateFn: (path: string) => boolean, +): Promise { + const typescriptFiles: Promise[] = []; + + const checkFilesInDirectory = (dir: string) => { + const files = fs.readdirSync(dir, {withFileTypes: true}); + for (const file of files) { + const fullPathToFile = join(dir, file.name); + const relativeFilePath = relative(baseDir, fullPathToFile); + + if ( + file.isFile() && + file.name.endsWith(TYPESCRIPT_EXTENSION) && + predicateFn(relativeFilePath) + ) { + typescriptFiles.push( + readFile(fullPathToFile, {encoding: 'utf-8'}).then((fileContent) => { + return { + path: relativeFilePath, + content: fileContent, + }; + }), + ); + } else if (file.isDirectory()) { + checkFilesInDirectory(fullPathToFile); + } + } + }; + + checkFilesInDirectory(baseDir); + + return Promise.all(typescriptFiles); +} + +/** Returns list of the `Standalone` @Component class names for given file */ +function analyzeFile(file: File): string[] { + const componentClassNames: string[] = []; + const sourceFile = ts.createSourceFile(file.path, file.content, ts.ScriptTarget.Latest, false); + + const visitNode = (node: ts.Node): void => { + if (ts.isClassDeclaration(node)) { + const decorators = ts.getDecorators(node); + const componentName = node.name ? node.name.text : null; + + if (decorators && decorators.length) { + for (const decorator of decorators) { + const call = decorator.expression; + + if ( + ts.isCallExpression(call) && + ts.isIdentifier(call.expression) && + call.expression.text === 'Component' && + call.arguments.length > 0 && + ts.isObjectLiteralExpression(call.arguments[0]) + ) { + const standaloneProperty = call.arguments[0].properties.find( + (property) => + property.name && + ts.isIdentifier(property.name) && + property.name.text === 'standalone', + ); + + const isStandalone = + !standaloneProperty || + (ts.isPropertyAssignment(standaloneProperty) && + standaloneProperty.initializer.kind === ts.SyntaxKind.TrueKeyword); + + if (isStandalone && componentName) { + componentClassNames.push(componentName); + } + } + } + } + } + + ts.forEachChild(node, visitNode); + }; + + visitNode(sourceFile); + + return componentClassNames; +} + +function generatePreviewsComponentMap(data: AnalyzedFiles[]): string { + let result = ''; + for (const fileData of data) { + for (const componentName of fileData.componentNames) { + const key = `adev/src/content/examples/${fileData.path}${ + fileData.componentNames.length > 1 ? '_' + componentName : '' + }`.replace(/\\/g, '/'); + result += `['${key}']: () => import('${EXAMPLES_PATH}/${fileData.path + .replace(/\\/g, '/') + .replace('.ts', '')}').then(c => c.${componentName}),\n`; + } + } + return fs.readFileSync(templateFilePath, 'utf8').replace(/\${previewsComponents}/g, result); +} diff --git a/adev/src/assets/previews/previews.ts b/adev/shared-docs/pipeline/examples/previews/previews.template similarity index 67% rename from adev/src/assets/previews/previews.ts rename to adev/shared-docs/pipeline/examples/previews/previews.template index 47feefedf2f..fefe533e58f 100644 --- a/adev/src/assets/previews/previews.ts +++ b/adev/shared-docs/pipeline/examples/previews/previews.template @@ -6,16 +6,12 @@ * found in the LICENSE file at https://angular.dev/license */ -/** - ****************************************************************************** - * DO NOT MANUALLY EDIT THIS FILE. THIS FILE IS AUTOMATICALLY GENERATED. - ****************************************************************************** - */ - import {Type} from '@angular/core'; /** * Map of the previews components, values are functions which returns the promise of the component type, which will be displayed as preview in the ExampleViewer component. * Keys has to be equal to paths written down in the docs markdown files. */ -export const PREVIEWS_COMPONENTS_MAP: Record Promise>> = {}; +export const PREVIEWS_COMPONENTS_MAP: Record Promise>> = { + ${previewsComponents} +}; diff --git a/adev/src/assets/previews/BUILD.bazel b/adev/src/assets/previews/BUILD.bazel index 167ecbe26ce..611c03b0000 100644 --- a/adev/src/assets/previews/BUILD.bazel +++ b/adev/src/assets/previews/BUILD.bazel @@ -1,15 +1,7 @@ -load("@build_bazel_rules_nodejs//:index.bzl", "copy_to_bin") +load("//adev/shared-docs:index.bzl", "generate_previews") -# TODO(josephperrott): Generate previews from source instead of using already generated files - -exports_files( - glob(["*"]), -) - -copy_to_bin( +generate_previews( name = "previews", - srcs = glob(["*"]), - visibility = [ - "//visibility:public", - ], + example_srcs = "//adev/src/content/examples", + visibility = ["//visibility:public"], ) diff --git a/adev/src/content/examples/BUILD.bazel b/adev/src/content/examples/BUILD.bazel new file mode 100644 index 00000000000..e9c5c059e24 --- /dev/null +++ b/adev/src/content/examples/BUILD.bazel @@ -0,0 +1,7 @@ +filegroup( + name = "examples", + srcs = [ + "//adev/src/content/examples/accessibility:ts_files", + ], + visibility = ["//visibility:public"], +) diff --git a/adev/src/content/examples/accessibility/BUILD.bazel b/adev/src/content/examples/accessibility/BUILD.bazel index 1aea2bfde54..62ad1acaf74 100644 --- a/adev/src/content/examples/accessibility/BUILD.bazel +++ b/adev/src/content/examples/accessibility/BUILD.bazel @@ -1,5 +1,26 @@ +load("//tools:defaults.bzl", "ng_module") + package(default_visibility = ["//visibility:public"]) +filegroup( + name = "ts_files", + srcs = glob(["src/app/**/*.ts"]), + visibility = ["//visibility:public"], +) + +ng_module( + name = "example", + srcs = [":ts_files"], + assets = glob([ + "src/app/**/*.html", + "src/app/**/*.css", + ]), + visibility = ["//visibility:public"], + deps = [ + "@npm//@angular/core", + ], +) + exports_files([ "src/app/app.component.html", "src/app/app.component.ts",