Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[dev-tool] Enable sample folders to use nested directory structures. #15110

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 59 additions & 22 deletions common/tools/dev-tool/src/commands/samples/publish.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ import instantiateSampleReadme from "../../templates/sampleReadme.md";
import { convert } from "./tsToJs";

import devToolPackageJson from "../../../package.json";
import { findMatchingFiles } from "../../util/findMatchingFiles";
import { EOL } from "os";

const log = createPrinter("publish");

Expand Down Expand Up @@ -194,23 +196,26 @@ async function processSources(
const tags = ts.getJSDocTags(node);

// Look for the @summary jsdoc tag block as well
if (summary === undefined && tags.some(({ tagName: { text } }) => text === "summary")) {
if (summary === undefined) {
for (const tag of tags) {
log.debug(`File ${relativeSourcePath} has tag ${tag.tagName.text}`);
if (tag.tagName.text === "summary") {
log.debug("Found summary tag on node:", node.getText(sourceFile));
// Replace is required due to multi-line splitting messing with table formatting
summary = tag.comment?.replace(/\s*\r?\n\s*/g, " ");
} else if (
tag.tagName.text.startsWith(`${AZSDK_META_TAG_PREFIX}`) &&
tag.comment !== undefined
) {
} else if (tag.tagName.text.startsWith(`${AZSDK_META_TAG_PREFIX}`)) {
// We ran into an `azsdk` directive in the metadata
const metaTag = tag.tagName.text.replace(
new RegExp(`^${AZSDK_META_TAG_PREFIX}`),
""
) as keyof AzSdkMetaTags;
log.debug(`File ${relativeSourcePath} has azsdk tag ${tag.tagName.text}`);
if (VALID_AZSDK_META_TAGS.includes(metaTag)) {
azSdkTags[metaTag as keyof AzSdkMetaTags] = JSON.parse(tag.comment);
const comment = tag.comment?.trim();
// If there was _no_ comment, then we can assume it is a boolean tag
// and so being specified at all is an indication that we should use
// `true`
azSdkTags[metaTag as keyof AzSdkMetaTags] = comment ? JSON.parse(comment) : true;
} else {
log.warn(
`Invalid azsdk tag ${metaTag}. Valid tags include ${VALID_AZSDK_META_TAGS}`
Expand All @@ -228,17 +233,26 @@ async function processSources(
return sourceFile;
};

const jsModuleText = convert(sourceText, {
fileName: source,
transformers: {
before: [sourceProcessor]
}
});

if (summary === undefined && azSdkTags.util !== true) {
log.debug(azSdkTags.util, summary);
fail(
`${relativeSourcePath} does not include an @summary tag and is not marked as a util (using @azsdk-util true).`
);
}

return {
filePath: source,
relativeSourcePath,
text: sourceText,
jsModuleText: convert(sourceText, {
fileName: source,
transformers: {
before: [sourceProcessor]
}
}),
summary: summary ?? fail(`${relativeSourcePath} does not include an @summary tag.`),
jsModuleText,
summary,
importedModules: importedModules.filter(isDependency),
usedEnvironmentVariables,
azSdkTags
Expand All @@ -248,6 +262,16 @@ async function processSources(
return Promise.all(jobs);
}

async function collect<T>(i: AsyncIterableIterator<T>): Promise<T[]> {
const out = [];

for await (const v of i) {
out.push(v);
}

return out;
}

/**
* Extracts the sample generation metainformation from the sample sources and
* configuration in package.json.
Expand All @@ -261,9 +285,10 @@ async function makeSampleGenerationInfo(
onError: () => void
): Promise<SampleGenerationInfo> {
const sampleSourcesPath = path.join(projectInfo.path, DEV_SAMPLES_BASE);
const sampleSources = (await fs.readdir(sampleSourcesPath))
.filter((name) => name.endsWith(".ts"))
.map((name) => path.join(sampleSourcesPath, name));

const sampleSources = await collect(
findMatchingFiles(sampleSourcesPath, (name) => name.endsWith(".ts"))
);

const sampleConfiguration = getSampleConfiguration(projectInfo.packageJson);

Expand Down Expand Up @@ -398,7 +423,8 @@ function createReadme(outputKind: OutputKind, info: SampleGenerationInfo): strin
},
publicationDirectory: PUBLIC_SAMPLES_BASE + "/" + info.topLevelDirectory,
useTypeScript: outputKind === OutputKind.TypeScript,
...info
...info,
moduleInfos: info.moduleInfos.filter((mod) => mod.summary !== undefined)
});
}

Expand Down Expand Up @@ -440,7 +466,18 @@ async function makeSamplesFactory(
*/
function postProcess(moduleText: string | Buffer): string {
const content = Buffer.isBuffer(moduleText) ? moduleText.toString("utf8") : moduleText;
return content.replace(new RegExp(`^\\s*\\*\\s*@${AZSDK_META_TAG_PREFIX}.*\n`, "gm"), "");
return (
content
.replace(new RegExp(`^\\s*\\*\\s*@${AZSDK_META_TAG_PREFIX}.*\n`, "gm"), "")
// We also need to clean up extra blank lines that might be left behind by
// removing azsdk tags. These regular expressions are extremely frustrating
// because they deal almost exclusively in the literal "/" and "*" characters.
.replace(/(\s+\*)+\//s, EOL + " */")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should add some tests for this logic, otherwise future me will definitely break it accidentally😄

// Clean up blank lines at the beginning
.replace(/\/\*\*(\s+\*)*/s, `/**${EOL} *`)
// Finally remove empty doc comments.
.replace(/\s*\/\*\*(\s+\*)*\/\s*/s, EOL + EOL)
);
}

// We use a tempdir at the outer layer to avoid creating dirty trees
Expand All @@ -455,8 +492,8 @@ async function makeSamplesFactory(
// We copy the samples sources in to the `src` folder on the typescript side
dir(
"src",
info.moduleInfos.map(({ filePath }) =>
file(path.basename(filePath), () => postProcess(fs.readFileSync(filePath)))
info.moduleInfos.map(({ relativeSourcePath, filePath }) =>
file(relativeSourcePath, () => postProcess(fs.readFileSync(filePath)))
)
)
]),
Expand All @@ -465,8 +502,8 @@ async function makeSamplesFactory(
file("package.json", () => jsonify(createPackageJson(info, OutputKind.JavaScript))),
copy("sample.env", path.join(projectInfo.path, "sample.env")),
// Extract the JS Module Text from the module info structures
...info.moduleInfos.map(({ filePath, jsModuleText }) =>
file(path.basename(filePath).replace(/\.ts$/, ".js"), () => postProcess(jsModuleText))
...info.moduleInfos.map(({ relativeSourcePath, jsModuleText }) =>
file(relativeSourcePath.replace(/\.ts$/, ".js"), () => postProcess(jsModuleText))
)
]),
// Copy extraFiles by reducing all configured destinations for each input file
Expand Down
23 changes: 10 additions & 13 deletions common/tools/dev-tool/src/config/rollup.base.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,25 +39,22 @@ export function openTelemetryCommonJs(): Record<string, string[]> {
];
}

const releasedOpenTelemetryVersions = [
"0.10.2",
"1.0.0-rc.0"
];
const releasedOpenTelemetryVersions = ["0.10.2", "1.0.0-rc.0"];

for (const version of releasedOpenTelemetryVersions) {
namedExports[
// working around a limitation in the rollup common.js plugin - it's not able to resolve these modules so the named exports listed above will not get applied. We have to drill down to the actual path.
`../../../common/temp/node_modules/.pnpm/@opentelemetry/api@${version}/node_modules/@opentelemetry/api/build/src/index.js`
] = [
"SpanKind",
"TraceFlags",
"getSpan",
"setSpan",
"StatusCode",
"CanonicalCode",
"getSpanContext",
"setSpanContext"
];
"SpanKind",
"TraceFlags",
"getSpan",
"setSpan",
"StatusCode",
"CanonicalCode",
"getSpanContext",
"setSpanContext"
];
}

return namedExports;
Expand Down
14 changes: 7 additions & 7 deletions common/tools/dev-tool/src/templates/sampleReadme.md.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,10 @@ function fence(language: string, ...contents: string[]): string {
* Ex. recognizePii.ts -> recognizepii
*/
function sampleLinkTag(filePath: string): string {
return path
.basename(filePath)
return filePath
.split(path.sep)
.join("_")
.replace(/\.[a-z]*$/, "")
.replace(/\//, "_")
.toLowerCase();
}

Expand All @@ -55,12 +55,12 @@ function fileLinks(info: SampleReadmeConfiguration) {
].join("/");

return filterModules(info)
.map(({ filePath, relativeSourcePath }) => {
.map(({ relativeSourcePath }) => {
const sourcePath = info.useTypeScript
? relativeSourcePath
: relativeSourcePath.replace(/\.ts$/, ".js");
return `[${sampleLinkTag(
filePath
relativeSourcePath
)}]: https://github.com/Azure/azure-sdk-for-js/blob/master/${packageSamplesPathFragment}/${sourcePath}`;
})
.join("\n");
Expand Down Expand Up @@ -115,11 +115,11 @@ function filterModules(info: SampleReadmeConfiguration): SampleReadmeConfigurati
* Renders the sample file table.
*/
function table(info: SampleReadmeConfiguration) {
const contents = filterModules(info).map(({ filePath, summary, relativeSourcePath }) => {
const contents = filterModules(info).map(({ summary, relativeSourcePath }) => {
const fileName = info.useTypeScript
? relativeSourcePath
: relativeSourcePath.replace(/\.ts$/, ".js");
return `| [${fileName}][${sampleLinkTag(filePath)}] | ${summary} |`;
return `| [${fileName}][${sampleLinkTag(relativeSourcePath)}] | ${summary} |`;
});

return [
Expand Down
6 changes: 5 additions & 1 deletion common/tools/dev-tool/src/util/fileTree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,5 +104,9 @@ export function file(name: string, contents: FileContents): FileTreeFactory {
: Buffer.from(immediateContents, "utf8");
};

return async (basePath) => fs.writeFile(path.join(basePath, name), getContentsAsBuffer());
return async (basePath) => {
const dirName = path.resolve(basePath, path.dirname(name));
await fs.ensureDir(dirName);
return fs.writeFile(path.join(basePath, name), getContentsAsBuffer());
};
}
7 changes: 6 additions & 1 deletion common/tools/dev-tool/src/util/sampleGenerationInfo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ export interface ModuleInfo {
/**
* The description provided by the first doc comment.
*/
summary: string;
summary?: string;
/**
* A list of module specifiers that are imported by this
* source file.
Expand Down Expand Up @@ -186,6 +186,10 @@ export interface AzSdkMetaTags {
* Causes the sample file to be ignored entirely (will skip publication).
*/
ignore?: boolean;
/**
* Causes the file to be omitted from the generated sample index (README).
*/
util?: boolean;
/**
* Causes the sample file to skip JavaScript output.
*/
Expand All @@ -206,6 +210,7 @@ export const AZSDK_META_TAG_PREFIX = "azsdk-";
export const VALID_AZSDK_META_TAGS: Array<keyof AzSdkMetaTags> = [
"weight",
"ignore",
"util",
"skip-javascript"
];

Expand Down