Skip to content

Commit 427bb5a

Browse files
mmalerbakirjs
authored andcommitted
docs: Add back script to generate example previews map (angular#60778)
Restores the ability to have example components rendered alongside the code. This functionality was broken a while back by angular#53511. To enable the embedded preview for an example, add `preview` and `path=adev/src/content/expamples/component/to/render.ts` attributes to the `<docs-code>` or `<docs-code-multifile>` tag. Tested with one of the accessibility examples and it seems to work now. PR Close angular#60778
1 parent 72dd133 commit 427bb5a

File tree

10 files changed

+297
-19
lines changed

10 files changed

+297
-19
lines changed

adev/BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ APPLICATION_ASSETS = [
4040
"//adev/src/assets/icons",
4141
"//adev/src/assets:api",
4242
"//adev/src/assets:content",
43+
"//adev/src/content/examples/accessibility:example",
4344
]
4445

4546
APPLICATION_DEPS = [

adev/shared-docs/index.bzl

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
load("//adev/shared-docs/pipeline:_guides.bzl", _generate_guides = "generate_guides")
22
load("//adev/shared-docs/pipeline:_navigation.bzl", _generate_nav_items = "generate_nav_items")
33
load("//adev/shared-docs/pipeline:_playground.bzl", _generate_playground = "generate_playground")
4+
load("//adev/shared-docs/pipeline:_previews.bzl", _generate_previews = "generate_previews")
45
load("//adev/shared-docs/pipeline:_stackblitz.bzl", _generate_stackblitz = "generate_stackblitz")
56
load("//adev/shared-docs/pipeline:_tutorial.bzl", _generate_tutorial = "generate_tutorial")
67

78
generate_guides = _generate_guides
89
generate_stackblitz = _generate_stackblitz
10+
generate_previews = _generate_previews
911
generate_playground = _generate_playground
1012
generate_tutorial = _generate_tutorial
1113
generate_nav_items = _generate_nav_items

adev/shared-docs/pipeline/BUILD.bazel

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,22 @@ esbuild_esm_bundle(
6161
],
6262
)
6363

64+
esbuild_esm_bundle(
65+
name = "previews-bundle",
66+
entry_point = "//adev/shared-docs/pipeline/examples/previews:index.ts",
67+
external = [
68+
"typescript",
69+
],
70+
output = "previews.mjs",
71+
platform = "node",
72+
target = "es2022",
73+
visibility = ["//visibility:public"],
74+
deps = [
75+
"//adev/shared-docs/pipeline/examples/previews:index",
76+
"@npm//typescript",
77+
],
78+
)
79+
6480
esbuild_esm_bundle(
6581
name = "zip-bundle",
6682
entry_point = "//adev/shared-docs/pipeline/examples/zip:index.ts",
@@ -112,6 +128,7 @@ esbuild_esm_bundle(
112128
exports_files([
113129
"_guides.bzl",
114130
"_stackblitz.bzl",
131+
"_previews.bzl",
115132
"_playground.bzl",
116133
"_tutorial.bzl",
117134
"_navigation.bzl",
@@ -129,6 +146,15 @@ nodejs_binary(
129146
visibility = ["//visibility:public"],
130147
)
131148

149+
nodejs_binary(
150+
name = "previews",
151+
data = [
152+
"@npm//typescript",
153+
],
154+
entry_point = "//adev/shared-docs/pipeline:previews.mjs",
155+
visibility = ["//visibility:public"],
156+
)
157+
132158
nodejs_binary(
133159
name = "zip",
134160
entry_point = "//adev/shared-docs/pipeline:zip.mjs",
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
load("@build_bazel_rules_nodejs//:providers.bzl", "run_node")
2+
3+
def _generate_previews(ctx):
4+
"""Implementation of the previews generator rule"""
5+
6+
# File declaration of the generated ts file
7+
ts_output = ctx.actions.declare_file("previews.ts")
8+
9+
# Set the arguments for the actions inputs and out put location.
10+
args = ctx.actions.args()
11+
12+
# Path to the examples for which previews are being generated.
13+
args.add(ctx.attr.example_srcs.label.package)
14+
15+
# Path to the preview map template.
16+
args.add(ctx.file.template_src)
17+
18+
# Path to the ts output file to write to.
19+
args.add(ts_output.path)
20+
21+
ctx.runfiles(files = ctx.files.template_src)
22+
23+
run_node(
24+
ctx = ctx,
25+
inputs = depset(ctx.files.example_srcs + ctx.files.template_src),
26+
executable = "_generate_previews",
27+
outputs = [ts_output],
28+
arguments = [args],
29+
)
30+
31+
# The return value describes what the rule is producing. In this case we need to specify
32+
# the "DefaultInfo" with the output ts file.
33+
return [DefaultInfo(files = depset([ts_output]))]
34+
35+
generate_previews = rule(
36+
# Point to the starlark function that will execute for this rule.
37+
implementation = _generate_previews,
38+
doc = """Rule that generates a map of example previews to their component""",
39+
40+
# The attributes that can be set to this rule.
41+
attrs = {
42+
"example_srcs": attr.label(
43+
doc = """Files used for the previews map generation.""",
44+
),
45+
"template_src": attr.label(
46+
doc = """The previews map template file to base the generated file on.""",
47+
default = Label("//adev/shared-docs/pipeline/examples/previews:template"),
48+
allow_single_file = True,
49+
),
50+
"_generate_previews": attr.label(
51+
default = Label("//adev/shared-docs/pipeline:previews"),
52+
executable = True,
53+
cfg = "exec",
54+
),
55+
},
56+
)
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
load("//tools:defaults.bzl", "ts_library")
2+
3+
package(default_visibility = ["//visibility:public"])
4+
5+
ts_library(
6+
name = "index",
7+
srcs = [
8+
"index.ts",
9+
],
10+
visibility = [
11+
"//adev/shared-docs:__subpackages__",
12+
],
13+
deps = [
14+
"@npm//@types/node",
15+
"@npm//typescript",
16+
],
17+
)
18+
19+
filegroup(
20+
name = "template",
21+
srcs = ["previews.template"],
22+
visibility = ["//visibility:public"],
23+
)
24+
25+
exports_files([
26+
"index.ts",
27+
"previews.template",
28+
])
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
/*!
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.dev/license
7+
*/
8+
9+
import * as fs from 'fs';
10+
import {readFile, writeFile} from 'fs/promises';
11+
import {join, relative} from 'path';
12+
import ts from 'typescript';
13+
14+
const [examplesDir, templateFilePath, outputFilePath] = process.argv.slice(2);
15+
16+
const TYPESCRIPT_EXTENSION = '.ts';
17+
const SKIP_FILES_WITH_EXTENSIONS = ['.e2e-spec.ts', '.spec.ts', '.po.ts'];
18+
const EXAMPLES_PATH = `../../content/examples`;
19+
20+
interface File {
21+
path: string;
22+
content: string;
23+
}
24+
25+
interface AnalyzedFiles {
26+
path: string;
27+
componentNames: string[];
28+
}
29+
30+
main();
31+
32+
async function main() {
33+
const files = await retrieveAllTypescriptFiles(
34+
examplesDir,
35+
(path) => !SKIP_FILES_WITH_EXTENSIONS.some((extensionToSkip) => path.endsWith(extensionToSkip)),
36+
);
37+
38+
const filesWithComponent = files
39+
.map((file) => ({
40+
componentNames: analyzeFile(file),
41+
path: file.path,
42+
}))
43+
.filter((result) => result.componentNames.length > 0);
44+
45+
const previewsComponentMap = generatePreviewsComponentMap(filesWithComponent);
46+
47+
await writeFile(outputFilePath, previewsComponentMap);
48+
}
49+
50+
/** Recursively search the provided directory for all typescript files and asynchronously load them. */
51+
function retrieveAllTypescriptFiles(
52+
baseDir: string,
53+
predicateFn: (path: string) => boolean,
54+
): Promise<File[]> {
55+
const typescriptFiles: Promise<File>[] = [];
56+
57+
const checkFilesInDirectory = (dir: string) => {
58+
const files = fs.readdirSync(dir, {withFileTypes: true});
59+
for (const file of files) {
60+
const fullPathToFile = join(dir, file.name);
61+
const relativeFilePath = relative(baseDir, fullPathToFile);
62+
63+
if (
64+
file.isFile() &&
65+
file.name.endsWith(TYPESCRIPT_EXTENSION) &&
66+
predicateFn(relativeFilePath)
67+
) {
68+
typescriptFiles.push(
69+
readFile(fullPathToFile, {encoding: 'utf-8'}).then((fileContent) => {
70+
return {
71+
path: relativeFilePath,
72+
content: fileContent,
73+
};
74+
}),
75+
);
76+
} else if (file.isDirectory()) {
77+
checkFilesInDirectory(fullPathToFile);
78+
}
79+
}
80+
};
81+
82+
checkFilesInDirectory(baseDir);
83+
84+
return Promise.all(typescriptFiles);
85+
}
86+
87+
/** Returns list of the `Standalone` @Component class names for given file */
88+
function analyzeFile(file: File): string[] {
89+
const componentClassNames: string[] = [];
90+
const sourceFile = ts.createSourceFile(file.path, file.content, ts.ScriptTarget.Latest, false);
91+
92+
const visitNode = (node: ts.Node): void => {
93+
if (ts.isClassDeclaration(node)) {
94+
const decorators = ts.getDecorators(node);
95+
const componentName = node.name ? node.name.text : null;
96+
97+
if (decorators && decorators.length) {
98+
for (const decorator of decorators) {
99+
const call = decorator.expression;
100+
101+
if (
102+
ts.isCallExpression(call) &&
103+
ts.isIdentifier(call.expression) &&
104+
call.expression.text === 'Component' &&
105+
call.arguments.length > 0 &&
106+
ts.isObjectLiteralExpression(call.arguments[0])
107+
) {
108+
const standaloneProperty = call.arguments[0].properties.find(
109+
(property) =>
110+
property.name &&
111+
ts.isIdentifier(property.name) &&
112+
property.name.text === 'standalone',
113+
);
114+
115+
const isStandalone =
116+
!standaloneProperty ||
117+
(ts.isPropertyAssignment(standaloneProperty) &&
118+
standaloneProperty.initializer.kind === ts.SyntaxKind.TrueKeyword);
119+
120+
if (isStandalone && componentName) {
121+
componentClassNames.push(componentName);
122+
}
123+
}
124+
}
125+
}
126+
}
127+
128+
ts.forEachChild(node, visitNode);
129+
};
130+
131+
visitNode(sourceFile);
132+
133+
return componentClassNames;
134+
}
135+
136+
function generatePreviewsComponentMap(data: AnalyzedFiles[]): string {
137+
let result = '';
138+
for (const fileData of data) {
139+
for (const componentName of fileData.componentNames) {
140+
const key = `adev/src/content/examples/${fileData.path}${
141+
fileData.componentNames.length > 1 ? '_' + componentName : ''
142+
}`.replace(/\\/g, '/');
143+
result += `['${key}']: () => import('${EXAMPLES_PATH}/${fileData.path
144+
.replace(/\\/g, '/')
145+
.replace('.ts', '')}').then(c => c.${componentName}),\n`;
146+
}
147+
}
148+
return fs.readFileSync(templateFilePath, 'utf8').replace(/\${previewsComponents}/g, result);
149+
}

adev/src/assets/previews/previews.ts renamed to adev/shared-docs/pipeline/examples/previews/previews.template

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,12 @@
66
* found in the LICENSE file at https://angular.dev/license
77
*/
88

9-
/**
10-
******************************************************************************
11-
* DO NOT MANUALLY EDIT THIS FILE. THIS FILE IS AUTOMATICALLY GENERATED.
12-
******************************************************************************
13-
*/
14-
159
import {Type} from '@angular/core';
1610

1711
/**
1812
* 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.
1913
* Keys has to be equal to paths written down in the docs markdown files.
2014
*/
21-
export const PREVIEWS_COMPONENTS_MAP: Record<string, () => Promise<Type<unknown>>> = {};
15+
export const PREVIEWS_COMPONENTS_MAP: Record<string, () => Promise<Type<unknown>>> = {
16+
${previewsComponents}
17+
};
Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,7 @@
1-
load("@build_bazel_rules_nodejs//:index.bzl", "copy_to_bin")
1+
load("//adev/shared-docs:index.bzl", "generate_previews")
22

3-
# TODO(josephperrott): Generate previews from source instead of using already generated files
4-
5-
exports_files(
6-
glob(["*"]),
7-
)
8-
9-
copy_to_bin(
3+
generate_previews(
104
name = "previews",
11-
srcs = glob(["*"]),
12-
visibility = [
13-
"//visibility:public",
14-
],
5+
example_srcs = "//adev/src/content/examples",
6+
visibility = ["//visibility:public"],
157
)
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
filegroup(
2+
name = "examples",
3+
srcs = [
4+
"//adev/src/content/examples/accessibility:ts_files",
5+
],
6+
visibility = ["//visibility:public"],
7+
)

adev/src/content/examples/accessibility/BUILD.bazel

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,26 @@
1+
load("//tools:defaults.bzl", "ng_module")
2+
13
package(default_visibility = ["//visibility:public"])
24

5+
filegroup(
6+
name = "ts_files",
7+
srcs = glob(["src/app/**/*.ts"]),
8+
visibility = ["//visibility:public"],
9+
)
10+
11+
ng_module(
12+
name = "example",
13+
srcs = [":ts_files"],
14+
assets = glob([
15+
"src/app/**/*.html",
16+
"src/app/**/*.css",
17+
]),
18+
visibility = ["//visibility:public"],
19+
deps = [
20+
"@npm//@angular/core",
21+
],
22+
)
23+
324
exports_files([
425
"src/app/app.component.html",
526
"src/app/app.component.ts",

0 commit comments

Comments
 (0)