From f029f6297c9c5243b46f8282232998a889238344 Mon Sep 17 00:00:00 2001 From: Richard Knoll Date: Wed, 10 Jun 2026 16:41:55 -0700 Subject: [PATCH 1/3] allow tutorials and examples to remove dependencies from blocksprj --- cli/cli.ts | 60 ++++++++++++++++++++++++++++------------------ pxtlib/tutorial.ts | 13 ++++++++++ webapp/src/app.tsx | 2 +- 3 files changed, 51 insertions(+), 24 deletions(-) diff --git a/cli/cli.ts b/cli/cli.ts index c16596e9fcb0..a385c2b3a89e 100644 --- a/cli/cli.ts +++ b/cli/cli.ts @@ -5839,7 +5839,9 @@ export async function buildCoreDeclarationFiles(parsed: commandParser.ParsedComm nodeutil.mkdirP(builtFolder); process.chdir(cwd); - const host = shareId ? new Host() : new SnippetHost("decl-build", { "main.ts" : "" }, { "blocksprj": "*" }); + const blocksprjConfig = readBlocksprjConfig(); + + const host = shareId ? new Host() : new SnippetHost("decl-build", { "main.ts" : "" }, { ...blocksprjConfig.dependencies }); const mainPkg = new pxt.MainPackage(host); if (shareId) { @@ -6223,6 +6225,16 @@ function checkDocsAsync(parsed?: commandParser.ParsedCommand): Promise { ) } +function readBlocksprjConfig(): pxt.PackageConfig { + const configPath = path.join("libs", pxt.BLOCKS_PROJECT_NAME, "pxt.json"); + + if (!nodeutil.fileExistsSync(configPath)) { + return undefined; + } + const config = nodeutil.readJson(configPath) as pxt.PackageConfig; + return config; +} + function checkFileSize(files: string[]): number { if (!pxt.appTarget.cloud) return 0; @@ -6262,6 +6274,8 @@ function internalCheckDocsAsync(compileSnippets?: boolean, re?: string, fix?: bo const existingSnippets: pxt.Map = {}; let snippets: CodeSnippet[] = []; + const blocksprjConfig = readBlocksprjConfig(); + const maxFileSize = checkFileSize(nodeutil.allFiles("docs", { maxDepth: 10, allowMissing: true, includeDirs: true, ignoredFileMarker: ".ignorelargefiles" })); if (!pxt.appTarget.ignoreDocsErrors && maxFileSize > (pxt.appTarget.cloud.maxFileSize || (30000000))) @@ -6434,7 +6448,7 @@ function internalCheckDocsAsync(compileSnippets?: boolean, re?: string, fix?: bo } // look for snippets - getCodeSnippets(entrypath, md).forEach((snippet, snipIndex) => addSnippet(snippet, entrypath, snipIndex, entrypath)); + getCodeSnippets(entrypath, md, blocksprjConfig).forEach((snippet, snipIndex) => addSnippet(snippet, entrypath, snipIndex, entrypath)); } nodeutil.mkdirP("temp"); @@ -6479,8 +6493,7 @@ function internalCheckDocsAsync(compileSnippets?: boolean, re?: string, fix?: bo continue; } const tutorial = pxt.tutorial.parseTutorial(tutorialMd); - const pkgs: pxt.Map = { "blocksprj": "*" }; - pxt.Util.jsonMergeFrom(pkgs, pxt.gallery.parsePackagesFromMarkdown(tutorialMd) || {}); + const pkgs = pxt.tutorial.mergeTutorialDependencies(blocksprjConfig.dependencies, pxt.gallery.parsePackagesFromMarkdown(tutorialMd) || {}); let extraFiles: Map = null; @@ -6497,7 +6510,7 @@ function internalCheckDocsAsync(compileSnippets?: boolean, re?: string, fix?: bo || (tutorial.language == "python")) { tutorial.steps .filter(step => !!step.contentMd) - .forEach((step, stepIndex) => getCodeSnippets(`${card.name}-step${stepIndex}`, step.contentMd) + .forEach((step, stepIndex) => getCodeSnippets(`${card.name}-step${stepIndex}`, step.contentMd, blocksprjConfig) .forEach((snippet, snippetIndex) => { snippet.packages = pkgs; snippet.extraFiles = extraFiles; @@ -6539,8 +6552,7 @@ function internalCheckDocsAsync(compileSnippets?: boolean, re?: string, fix?: bo continue; } const prj = pxt.gallery.parseExampleMarkdown(card.name, exMd); - const pkgs: pxt.Map = { "blocksprj": "*" }; - pxt.U.jsonMergeFrom(pkgs, prj.dependencies); + const pkgs = pxt.tutorial.mergeTutorialDependencies(blocksprjConfig.dependencies, prj.dependencies); let extraFiles: Map = undefined; @@ -6616,6 +6628,8 @@ function internalCacheUsedBlocksAsync(): Promise> { const mdRegex = /\.md$/; const targetDirs = pxt.appTarget.cacheusedblocksdirs; const builtTututorialInfo: Map = {}; + const blocksprjConfig = readBlocksprjConfig(); + if (targetDirs) { targetDirs.forEach(dir => { pxt.log(`looking for tutorial markdown in ${dir}`); @@ -6633,8 +6647,9 @@ function internalCacheUsedBlocksAsync(): Promise> { pxt.log(`error resolving tutorial markdown at ${path}`); } const tutorial = pxt.tutorial.parseTutorial(md) as TutorialInfo; - const pkgs: pxt.Map = { "blocksprj": "*" }; - pxt.Util.jsonMergeFrom(pkgs, pxt.gallery.parsePackagesFromMarkdown(md) || {}); + const tutorialDeps = pxt.gallery.parsePackagesFromMarkdown(md) || {}; + const pkgs: pxt.Map = pxt.tutorial.mergeTutorialDependencies(blocksprjConfig.dependencies, tutorialDeps); + tutorial.pkgs = pkgs; tutorial.path = path; @@ -6752,7 +6767,7 @@ export interface CodeSnippet { src?: string; } -export function getCodeSnippets(fileName: string, md: string): CodeSnippet[] { +export function getCodeSnippets(fileName: string, md: string, baseConfig: pxt.PackageConfig): CodeSnippet[] { const supported: pxt.Map = { "blocks": "ts", "block": "ts", @@ -6778,21 +6793,20 @@ export function getCodeSnippets(fileName: string, md: string): CodeSnippet[] { }; } - - const pkgs: pxt.Map = { - "blocksprj": "*" - } - snippets.filter(snip => snip.type == "package") + const tutorialDeps = snippets.filter(snip => snip.type == "package") .map(snip => snip.code.split('\n')) - .forEach(lines => lines - .map(l => l.replace(/\s*$/, '')) - .filter(line => !!line) - .forEach(line => { + .reduce((acc, lines) => { + for (let line of lines) { + line = line.replace(/\s*$/, ''); + if (!line) continue; const i = line.indexOf('='); - if (i < 0) pkgs[line] = "*"; - else pkgs[line.substring(0, i)] = line.substring(i + 1); - }) - ); + if (i < 0) acc[line] = "*"; + else acc[line.substring(0, i).trim()] = line.substring(i + 1); + } + return acc; + }, {} as pxt.Map); + + const pkgs = pxt.tutorial.mergeTutorialDependencies(baseConfig.dependencies, tutorialDeps); const pkgName = fileName.replace(/\\/g, '-').replace(/.md$/i, ''); return codeSnippets.map((snip, i) => { diff --git a/pxtlib/tutorial.ts b/pxtlib/tutorial.ts index edf671a77736..0f706e5ea5e9 100644 --- a/pxtlib/tutorial.ts +++ b/pxtlib/tutorial.ts @@ -619,4 +619,17 @@ ${code} if (!metadata) return false; return !!(metadata.hideFromProjects || metadata.hideIteration); } + + export function mergeTutorialDependencies(projectDependencies: pxt.Map, tutorialDependencies: pxt.Map): pxt.Map { + const merged: pxt.Map = { ...projectDependencies }; + for (const dep of Object.keys(tutorialDependencies)) { + const ver = tutorialDependencies[dep]; + if (ver.toLowerCase() === "remove") { + delete merged[dep]; + } else { + merged[dep] = ver; + } + } + return merged; + } } diff --git a/webapp/src/app.tsx b/webapp/src/app.tsx index 51c6f757e1b7..72869645c4db 100644 --- a/webapp/src/app.tsx +++ b/webapp/src/app.tsx @@ -3079,7 +3079,7 @@ export class ProjectView } if (options.dependencies) { - Util.jsonMergeFrom(cfg.dependencies, options.dependencies) + cfg.dependencies = pxt.tutorial.mergeTutorialDependencies(cfg.dependencies, options.dependencies); } if (options.extensionUnderTest) { const ext = workspace.getHeader(options.extensionUnderTest); From 6bf18d99caee4b64eb32ad1187782303195d6e3c Mon Sep 17 00:00:00 2001 From: Richard Knoll Date: Wed, 10 Jun 2026 16:56:11 -0700 Subject: [PATCH 2/3] update docs --- docs/writing-docs/tutorials/basics.md | 56 ++++++++++++++++++++++++++- 1 file changed, 54 insertions(+), 2 deletions(-) diff --git a/docs/writing-docs/tutorials/basics.md b/docs/writing-docs/tutorials/basics.md index 6d6ddbe27048..34179e22eb0e 100644 --- a/docs/writing-docs/tutorials/basics.md +++ b/docs/writing-docs/tutorials/basics.md @@ -289,7 +289,7 @@ The `BlocksExistValidator` also has an `Enabled` property that determines whethe ``` ```` -**Enable the `BlocksExistValidator` globally and ignore highlighted blocks** +**Enable the `BlocksExistValidator` globally and ignore highlighted blocks** _Note: highlight is not specified in the markers property._ ```` ```validation.global @@ -306,7 +306,7 @@ _Note: highlight is not specified in the markers property._ ``` ```` ## Accordion/hidden hints -If you want to provide extra information without having to divert the coder's attention, you can include content in an "accordion" style hint control. +If you want to provide extra information without having to divert the coder's attention, you can include content in an "accordion" style hint control. ### ~ hint If you want your hint to display by default when a step is encountered see [Explicit Hints](/writing-docs/tutorials/control-options#explicit-hints). @@ -383,3 +383,55 @@ If your tutorial requires the use of an extension, you can add it using the [pac microturtle=github:microsoft/pxt-microturtle ``` ```` + +These dependencies will be merged into the dependencies of the default project (i.e. the project you get when you click "New Project" on the home page). + +For example, in pxt-microbit the default project's dependencies look like this: + +```json +{ + "dependencies": { + "core": "*", + "radio": "*", + "microphone": "*" + } +} +``` + +With the above package annotation, the dependencies would be updated to include and entry for `microturtle` like so: + +```json +{ + "dependencies": { + "core": "*", + "radio": "*", + "microphone": "*", + "microturtle": "microsoft/pxt-microturtle" + } +} +``` + +If your tutorial needs to remove one of the default dependencies from the project, you can do so by placing the word "remove" after the equals sign. + +For example, the `bluetooth` extension in pxt-microbit is not compatible with the default `radio` extension. If we wanted to author a `bluetooth` tutorial, we would need to remove the `radio` extension like so: + +```` +```packages +bluetooth +radio=remove +``` +```` + +which would result in: + +```json +{ + "dependencies": { + "core": "*", + "microphone": "*", + "bluetooth": "*" + } +} +``` + +In general, it's better not to remove any dependencies from the project unless absolutely required! \ No newline at end of file From 2095941f09a76e182277b35885a4da4cbc7b4aa2 Mon Sep 17 00:00:00 2001 From: Richard Knoll Date: Wed, 10 Jun 2026 17:00:08 -0700 Subject: [PATCH 3/3] typo --- docs/writing-docs/tutorials/basics.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/writing-docs/tutorials/basics.md b/docs/writing-docs/tutorials/basics.md index 34179e22eb0e..570f85c23bd7 100644 --- a/docs/writing-docs/tutorials/basics.md +++ b/docs/writing-docs/tutorials/basics.md @@ -416,7 +416,7 @@ If your tutorial needs to remove one of the default dependencies from the projec For example, the `bluetooth` extension in pxt-microbit is not compatible with the default `radio` extension. If we wanted to author a `bluetooth` tutorial, we would need to remove the `radio` extension like so: ```` -```packages +```package bluetooth radio=remove ```