├── packages ├── makecode-core │ ├── simloader │ │ ├── custom.js │ │ ├── tsconfig.json │ │ ├── build.js │ │ ├── index.html │ │ └── loader.ts │ ├── tsconfig.json │ ├── src │ │ ├── stackresolver.ts │ │ ├── host.ts │ │ ├── semver.ts │ │ ├── share.ts │ │ ├── files.ts │ │ ├── loader.ts │ │ ├── service.ts │ │ ├── downloader.ts │ │ └── mkc.ts │ ├── package.json │ └── external │ │ └── pxtpackage.d.ts ├── makecode-node │ ├── makecode │ ├── tsconfig.json │ ├── package.json │ └── src │ │ ├── simserver.ts │ │ ├── deploy.ts │ │ ├── nodeHost.ts │ │ ├── bump.ts │ │ ├── languageService.ts │ │ └── cli.ts └── makecode-browser │ ├── worker │ ├── tsconfig.json │ ├── build.js │ ├── pxt.d.ts │ └── worker.ts │ ├── tsconfig.json │ ├── package.json │ └── src │ ├── types.d.ts │ └── languageService.ts ├── .prettierrc ├── Makefile ├── test ├── main.ts └── pxt.json ├── .vscode ├── extensions.json ├── settings.json ├── launch.json └── tasks.json ├── .gitignore ├── package.json ├── CODE_OF_CONDUCT.md ├── LICENSE ├── .github └── workflows │ ├── is-vtag.yml │ ├── build.yml │ ├── check-if-merged-pr.yml │ └── tag-bump-commit.yml ├── SECURITY.md ├── README.md └── scripts └── release.js /packages/makecode-core/simloader/custom.js: -------------------------------------------------------------------------------- 1 | // can be replaced by assets/custom.js 2 | -------------------------------------------------------------------------------- /packages/makecode-node/makecode: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | require('./built/cli.js') 3 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "arrowParens": "avoid", 3 | "semi": false, 4 | "tabWidth": 4 5 | } -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | all: 2 | npm run compile 3 | 4 | bump: all 5 | npm version patch 6 | 7 | pub: bump 8 | npm publish 9 | -------------------------------------------------------------------------------- /test/main.ts: -------------------------------------------------------------------------------- 1 | basic.forever(function () { 2 | basic.showIcon(IconNames.Heart) 3 | basic.showIcon(IconNames.Heart) 4 | }) 5 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "esbenp.prettier-vscode", 4 | "dbaeumer.vscode-eslint" 5 | ] 6 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | tmp 2 | built 3 | .DS_Store 4 | node_modules 5 | pxt_modules 6 | *.vsix 7 | src/simloaderfiles.ts 8 | packages/makecode-core/src/simloaderfiles.ts 9 | packages/makecode-browser/src/workerFiles.ts 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "private": true, 4 | "scripts": { 5 | }, 6 | "devDependencies": { 7 | }, 8 | "workspaces": [ 9 | "packages/makecode-core", 10 | "packages/makecode-browser", 11 | "packages/makecode-node" 12 | ] 13 | } -------------------------------------------------------------------------------- /packages/makecode-browser/worker/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "noImplicitReturns": true, 4 | "target": "ES2017", 5 | "outDir": "built", 6 | "lib": ["es6", "dom"], 7 | "sourceMap": false, 8 | "rootDir": "." 9 | }, 10 | "exclude": ["node_modules", ".vscode-test"], 11 | "include": [ 12 | "*.ts" 13 | ] 14 | } 15 | 16 | -------------------------------------------------------------------------------- /packages/makecode-core/simloader/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "noImplicitReturns": true, 4 | "target": "ES2017", 5 | "outDir": "built", 6 | "lib": ["es6", "dom"], 7 | "sourceMap": false, 8 | "rootDir": "." 9 | }, 10 | "exclude": ["node_modules", ".vscode-test"], 11 | "include": [ 12 | "*.ts" 13 | ] 14 | } 15 | 16 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Microsoft Open Source Code of Conduct 2 | 3 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 4 | 5 | Resources: 6 | 7 | - [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/) 8 | - [Microsoft Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) 9 | - Contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with questions or concerns 10 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | // Place your settings in this file to overwrite default and user settings. 2 | { 3 | "files.exclude": { 4 | "built": false // set this to true to hide the "out" folder with the compiled JS files 5 | }, 6 | "search.exclude": { 7 | "built": true // set this to false to include "out" folder in search results 8 | }, 9 | "typescript.tsdk": "./node_modules/typescript/lib" // we want to use the TS server from our node_modules folder to control its version 10 | } 11 | -------------------------------------------------------------------------------- /test/pxt.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test", 3 | "description": "", 4 | "dependencies": { 5 | "core": "*", 6 | "radio": "*", 7 | "microphone": "*", 8 | "jacdac": "github:microsoft/pxt-jacdac", 9 | "jacdac-button": "github:microsoft/pxt-jacdac/button#v0.8.18" 10 | }, 11 | "files": [ 12 | "main.ts" 13 | ], 14 | "public": true, 15 | "supportedTargets": [ 16 | "microbit" 17 | ], 18 | "binaryonly": true, 19 | "version": "0.7.0" 20 | } 21 | -------------------------------------------------------------------------------- /packages/makecode-browser/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "noImplicitAny": true, 4 | "noImplicitReturns": true, 5 | "module": "commonjs", 6 | "target": "ES2017", 7 | "outDir": "built", 8 | "lib": ["es6", "DOM"], 9 | "sourceMap": true, 10 | "types": ["node"], 11 | "rootDir": "src", 12 | "declaration": true 13 | }, 14 | "exclude": ["node_modules", ".vscode-test"], 15 | "include": [ 16 | "src/*.ts" 17 | ] 18 | } 19 | 20 | -------------------------------------------------------------------------------- /packages/makecode-core/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "noImplicitAny": true, 4 | "noImplicitReturns": true, 5 | "module": "commonjs", 6 | "target": "ES2017", 7 | "outDir": "built", 8 | "lib": ["es6", "DOM"], 9 | "sourceMap": true, 10 | "types": ["node"], 11 | "rootDir": "src", 12 | "declaration": true 13 | }, 14 | "exclude": ["node_modules", ".vscode-test"], 15 | "include": [ 16 | "src/*.ts" 17 | ] 18 | } 19 | 20 | -------------------------------------------------------------------------------- /packages/makecode-node/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "noImplicitAny": true, 4 | "noImplicitReturns": true, 5 | "module": "commonjs", 6 | "target": "ES2017", 7 | "outDir": "built", 8 | "lib": ["es6", "DOM"], 9 | "sourceMap": true, 10 | "types": ["node"], 11 | "rootDir": "src", 12 | "declaration": true 13 | }, 14 | "exclude": ["node_modules", ".vscode-test"], 15 | "include": [ 16 | "src/*.ts" 17 | ] 18 | } 19 | 20 | -------------------------------------------------------------------------------- /packages/makecode-browser/worker/build.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | const root = "worker/"; 3 | 4 | const outFile = "src/workerFiles.ts"; 5 | 6 | if (process.argv[2] === "clean") { 7 | clean() 8 | } 9 | else { 10 | build(); 11 | } 12 | 13 | function build() { 14 | let res = "export const workerJs = `\n"; 15 | 16 | const text = fs.readFileSync(root + "built/worker.js", "utf8"); 17 | res += text.replace(/[\\`$]/g, x => "\\" + x); 18 | res += "`;\n"; 19 | 20 | console.log(`generate ${outFile}; ${res.length} bytes`); 21 | fs.writeFileSync(outFile, res); 22 | } 23 | 24 | function clean() { 25 | try { 26 | fs.unlinkSync(outFile); 27 | } 28 | catch (e) { 29 | // ignore 30 | } 31 | } 32 | 33 | -------------------------------------------------------------------------------- /packages/makecode-core/simloader/build.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | const root = "simloader/"; 3 | 4 | const outFile = "src/simloaderfiles.ts"; 5 | 6 | if (process.argv[2] === "clean") { 7 | clean() 8 | } 9 | else { 10 | build(); 11 | } 12 | 13 | function build() { 14 | let res = "export const simloaderFiles: Record = {\n"; 15 | function addFile(id, fn) { 16 | const f = fs.readFileSync(root + fn, "utf8"); 17 | res += `"${id}": \`` + f.replace(/[\\`$]/g, x => "\\" + x) + "`,\n"; 18 | } 19 | 20 | addFile("loader.js", "built/loader.js"); 21 | addFile("index.html", "index.html"); 22 | addFile("custom.js", "custom.js"); 23 | 24 | res += "}\n"; 25 | console.log(`generate ${outFile}; ${res.length} bytes`); 26 | fs.writeFileSync(outFile, res); 27 | } 28 | 29 | function clean() { 30 | try { 31 | fs.unlinkSync(outFile); 32 | } 33 | catch (e) { 34 | // ignore 35 | } 36 | } 37 | 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Microsoft Corporation. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE 22 | -------------------------------------------------------------------------------- /.github/workflows/is-vtag.yml: -------------------------------------------------------------------------------- 1 | name: Whether the tag is a semver tag 2 | 3 | on: 4 | workflow_call: 5 | outputs: 6 | is_vtag: 7 | description: 'Whether the tag is a semver tag' 8 | value: ${{ jobs.filter-vtags.outputs.is_vtag }} 9 | 10 | jobs: 11 | filter-vtags: 12 | runs-on: ubuntu-latest 13 | outputs: 14 | is_vtag: ${{ steps.check-tag.outputs.is_vtag }} 15 | tag: ${{ steps.check-tag.outputs.tag }} 16 | steps: 17 | - name: Inputs 18 | run: | 19 | echo "GITHUB_REF_TYPE=${GITHUB_REF_TYPE}" 20 | echo "GITHUB_REF_NAME=${GITHUB_REF_NAME}" 21 | - name: Check tag pattern 22 | id: check-tag 23 | run: | 24 | if [[ "${GITHUB_REF_TYPE}" == "tag" && "${GITHUB_REF_NAME}" =~ ^\(makecode-core|makecode-browser|makecode\)-v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then 25 | echo "is_vtag=true" >> "$GITHUB_OUTPUT" 26 | echo "tag=${GITHUB_REF_NAME}" >> "$GITHUB_OUTPUT" 27 | else 28 | echo "is_vtag=false" >> "$GITHUB_OUTPUT" 29 | fi 30 | - name: Outputs 31 | run: echo "Step output is_vtag = ${{ steps.check-tag.outputs.is_vtag }}" && echo "Step output tag = ${{ steps.check-tag.outputs.tag }}" 32 | -------------------------------------------------------------------------------- /packages/makecode-browser/worker/pxt.d.ts: -------------------------------------------------------------------------------- 1 | declare namespace pxt { 2 | type Map = { 3 | [index: string]: T; 4 | }; 5 | 6 | let appTarget: any; 7 | let webConfig: any; 8 | let options: any; 9 | 10 | interface SimpleDriverCallbacks { 11 | cacheGet: (key: string) => Promise; 12 | cacheSet: (key: string, val: string) => Promise; 13 | httpRequestAsync?: (options: any) => Promise; 14 | pkgOverrideAsync?: (id: string) => Promise>; 15 | } 16 | interface SimpleCompileOptions { 17 | native?: boolean; 18 | } 19 | function simpleInstallPackagesAsync(files: pxt.Map): Promise; 20 | function simpleGetCompileOptionsAsync(files: pxt.Map, simpleOptions: SimpleCompileOptions): Promise; 21 | function setupSimpleCompile(cfg?: SimpleDriverCallbacks): void; 22 | function setHwVariant(variant: string): void; 23 | function getHwVariants(): any[]; 24 | function savedAppTheme(): any; 25 | function reloadAppTargetVariant(): void; 26 | function setCompileSwitches(flags: string): void; 27 | function setupWebConfig(config: any): void; 28 | } 29 | 30 | declare namespace pxtc.service { 31 | function performOperation(op: string, data: any): any; 32 | } 33 | 34 | declare namespace ts.pxtc.assembler { 35 | let debug: boolean; 36 | } -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | // A launch configuration that compiles the extension and then opens it inside a new window 2 | { 3 | "version": "0.1.0", 4 | "configurations": [ 5 | { 6 | "name": "Launch Extension", 7 | "type": "extensionHost", 8 | "request": "launch", 9 | "runtimeExecutable": "${execPath}", 10 | "args": ["--extensionDevelopmentPath=${workspaceRoot}/vscode" ], 11 | "stopOnEntry": false, 12 | "sourceMaps": true, 13 | "outDir": "${workspaceRoot}/vscode/built", 14 | "preLaunchTask": "npm" 15 | }, 16 | { 17 | "name": "init", 18 | "type": "node", 19 | "request": "launch", 20 | "program": "${workspaceRoot}/packages/makecode-node/built/cli.js", 21 | "stopOnEntry": false, 22 | "args": [ 23 | "init", 24 | "arcade" 25 | ], 26 | "cwd": "${workspaceRoot}/../vscode-test2", 27 | "runtimeExecutable": null, 28 | "runtimeArgs": [ 29 | "--nolazy" 30 | ], 31 | "env": { 32 | "NODE_ENV": "development" 33 | }, 34 | "console": "integratedTerminal", 35 | "sourceMaps": false, 36 | "outFiles": [] 37 | }, 38 | ] 39 | } 40 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | // Available variables which can be used inside of strings. 2 | // ${workspaceRoot}: the root folder of the team 3 | // ${file}: the current opened file 4 | // ${fileBasename}: the current opened file's basename 5 | // ${fileDirname}: the current opened file's dirname 6 | // ${fileExtname}: the current opened file's extension 7 | // ${cwd}: the current working directory of the spawned process 8 | 9 | // A task runner that calls a custom npm script that compiles the extension. 10 | { 11 | "version": "2.0.0", 12 | 13 | // we want to run npm 14 | "command": "npm", 15 | 16 | // we run the custom script "compile" as defined in package.json 17 | "args": ["run", "compile", "--loglevel", "silent"], 18 | 19 | // The tsc compiler is started in watching mode 20 | "isWatching": true, 21 | 22 | // use the standard tsc in watch mode problem matcher to find compile problems in the output. 23 | "problemMatcher": "$tsc-watch", 24 | "tasks": [ 25 | { 26 | "label": "npm", 27 | "type": "shell", 28 | "command": "npm", 29 | "args": [ 30 | "run", 31 | "compile", 32 | "--loglevel", 33 | "silent" 34 | ], 35 | "isBackground": true, 36 | "problemMatcher": "$tsc-watch", 37 | "group": "build" 38 | } 39 | ] 40 | } -------------------------------------------------------------------------------- /packages/makecode-core/src/stackresolver.ts: -------------------------------------------------------------------------------- 1 | export function resolveAddr(sourceMap: Record, addr: number) { 2 | const offsets = [-2, -4, 0] 3 | let hit = "" 4 | let bestOffset: number = undefined 5 | if (addr == 2) return "" 6 | for (const fn of Object.keys(sourceMap)) { 7 | const vals = sourceMap[fn] 8 | for (let i = 0; i < vals.length; i += 3) { 9 | const lineNo = vals[i] 10 | const startA = vals[i + 1] 11 | const endA = startA + vals[i + 2] 12 | if (addr + 10 >= startA && addr - 10 <= endA) { 13 | for (const off of offsets) { 14 | if (startA <= addr + off && addr + off <= endA) { 15 | if ( 16 | !hit || 17 | offsets.indexOf(off) < offsets.indexOf(bestOffset) 18 | ) { 19 | hit = fn + "(" + lineNo + ")" 20 | bestOffset = off 21 | } 22 | } 23 | } 24 | } 25 | } 26 | } 27 | return hit 28 | } 29 | 30 | export function expandStackTrace( 31 | sourceMap: Record, 32 | stackTrace: string 33 | ) { 34 | return stackTrace.replace(/(^| )PC:0x([A-F0-9]+)/g, (full, space, num) => { 35 | const n = resolveAddr(sourceMap, parseInt(num, 16)) || "???" 36 | return " " + n + " (0x" + num + ")" 37 | }) 38 | } 39 | -------------------------------------------------------------------------------- /packages/makecode-browser/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "makecode-browser", 3 | "version": "1.3.5", 4 | "description": "MakeCode (PXT) - web-cached build tool", 5 | "keywords": [ 6 | "TypeScript", 7 | "JavaScript", 8 | "education", 9 | "microbit", 10 | "arcade", 11 | "makecode" 12 | ], 13 | "repository": { 14 | "type": "git", 15 | "url": "https://github.com/microsoft/pxt-mkc" 16 | }, 17 | "author": "", 18 | "license": "MIT", 19 | "homepage": "https://github.com/microsoft/pxt-mkc", 20 | "files": [ 21 | "README.md", 22 | "built/*", 23 | "external/*", 24 | "makecode" 25 | ], 26 | "preferGlobal": false, 27 | "engines": { 28 | "node": ">= 14.0.0" 29 | }, 30 | "scripts": { 31 | "prebuild": "npm run worker", 32 | "build": "tsc --build", 33 | "compile": "npm run build", 34 | "clean": "tsc --build --clean && tsc --build --clean worker && npm run clean-worker", 35 | "clean-worker": "node worker/build.js clean", 36 | "watch": "npm run worker && tsc --build --watch", 37 | "worker": "tsc --build worker && node worker/build.js" 38 | }, 39 | "main": "built/mkc.js", 40 | "devDependencies": { 41 | "typescript": "^4.4.3" 42 | }, 43 | "dependencies": { 44 | "makecode-core": "^1.7.3" 45 | }, 46 | "release": { 47 | "branch": "master", 48 | "plugins": [ 49 | "@semantic-release/commit-analyzer", 50 | "@semantic-release/release-notes-generator", 51 | [ 52 | "@semantic-release/github", 53 | { 54 | "successComment": false, 55 | "failComment": false 56 | } 57 | ], 58 | "@semantic-release/npm", 59 | [ 60 | "@semantic-release/git", 61 | { 62 | "assets": [ 63 | "package.json", 64 | "package-lock.json" 65 | ] 66 | } 67 | ] 68 | ] 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /packages/makecode-core/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "makecode-core", 3 | "version": "1.7.9", 4 | "description": "MakeCode (PXT) - web-cached build tool", 5 | "keywords": [ 6 | "TypeScript", 7 | "JavaScript", 8 | "education", 9 | "microbit", 10 | "arcade", 11 | "makecode" 12 | ], 13 | "repository": { 14 | "type": "git", 15 | "url": "https://github.com/microsoft/pxt-mkc" 16 | }, 17 | "author": "", 18 | "license": "MIT", 19 | "homepage": "https://github.com/microsoft/pxt-mkc", 20 | "files": [ 21 | "README.md", 22 | "built/*", 23 | "external/*" 24 | ], 25 | "bin": {}, 26 | "engines": { 27 | "node": ">= 14.0.0" 28 | }, 29 | "scripts": { 30 | "prebuild": "npm run sim", 31 | "build": "tsc --build", 32 | "compile": "npm run build", 33 | "clean": "tsc --build --clean && tsc --build --clean simloader && npm run clean-simloader", 34 | "clean-simloader": "node simloader/build.js clean", 35 | "watch": "npm run sim && tsc --build --watch", 36 | "sim": "tsc -p simloader && node simloader/build.js" 37 | }, 38 | "main": "built/mkc.js", 39 | "devDependencies": { 40 | "@types/node": "^16.10.3", 41 | "@types/semver": "^7.3.9", 42 | "typescript": "^4.4.3" 43 | }, 44 | "dependencies": { 45 | "@xmldom/xmldom": "^0.9.8", 46 | "chalk": "^4.1.2" 47 | }, 48 | "release": { 49 | "branch": "master", 50 | "plugins": [ 51 | "@semantic-release/commit-analyzer", 52 | "@semantic-release/release-notes-generator", 53 | [ 54 | "@semantic-release/github", 55 | { 56 | "successComment": false, 57 | "failComment": false 58 | } 59 | ], 60 | "@semantic-release/npm", 61 | [ 62 | "@semantic-release/git", 63 | { 64 | "assets": [ 65 | "package.json", 66 | "package-lock.json" 67 | ] 68 | } 69 | ] 70 | ] 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /packages/makecode-node/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "makecode", 3 | "version": "1.3.5", 4 | "description": "MakeCode (PXT) - web-cached build tool", 5 | "keywords": [ 6 | "TypeScript", 7 | "JavaScript", 8 | "education", 9 | "microbit", 10 | "arcade", 11 | "makecode" 12 | ], 13 | "repository": { 14 | "type": "git", 15 | "url": "https://github.com/microsoft/pxt-mkc" 16 | }, 17 | "author": "", 18 | "license": "MIT", 19 | "homepage": "https://github.com/microsoft/pxt-mkc", 20 | "files": [ 21 | "README.md", 22 | "built/*", 23 | "external/*", 24 | "makecode" 25 | ], 26 | "preferGlobal": true, 27 | "bin": { 28 | "makecode": "./makecode", 29 | "mkc": "./makecode" 30 | }, 31 | "engines": { 32 | "node": ">= 14.0.0" 33 | }, 34 | "scripts": { 35 | "build": "tsc --build", 36 | "compile": "npm run build", 37 | "watch": "tsc --build --watch", 38 | "clean": "tsc --build --clean" 39 | }, 40 | "main": "built/cli.js", 41 | "devDependencies": { 42 | "@types/glob": "^7.1.4", 43 | "@types/node": "^16.10.3", 44 | "@types/semver": "^7.3.9", 45 | "typescript": "^4.4.3" 46 | }, 47 | "dependencies": { 48 | "chalk": "^4.1.2", 49 | "commander": "^8.2.0", 50 | "glob": "^7.2.0", 51 | "node-watch": "^0.7.2", 52 | "semver": "^7.3.7", 53 | "makecode-core": "^1.7.3" 54 | }, 55 | "release": { 56 | "branch": "master", 57 | "plugins": [ 58 | "@semantic-release/commit-analyzer", 59 | "@semantic-release/release-notes-generator", 60 | [ 61 | "@semantic-release/github", 62 | { 63 | "successComment": false, 64 | "failComment": false 65 | } 66 | ], 67 | "@semantic-release/npm", 68 | [ 69 | "@semantic-release/git", 70 | { 71 | "assets": [ 72 | "package.json", 73 | "package-lock.json" 74 | ] 75 | } 76 | ] 77 | ] 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: 4 | push: 5 | branches: 6 | - '**' # Run workflow when any branch is updated 7 | tags: 8 | - '*' # Run workflow when any new tag is pushed 9 | pull_request: 10 | branches: 11 | - '**' # Run workflow for pull requests targeting any branch 12 | 13 | permissions: 14 | contents: write 15 | id-token: write # Required for OIDC 16 | 17 | jobs: 18 | 19 | filter-vtags: 20 | uses: ./.github/workflows/is-vtag.yml 21 | 22 | tag-bump-commit: 23 | uses: ./.github/workflows/tag-bump-commit.yml 24 | needs: filter-vtags 25 | if: fromJSON(needs.filter-vtags.outputs.is_vtag || 'false') == false 26 | 27 | build: 28 | name: buildpush 29 | runs-on: ubuntu-latest 30 | needs: tag-bump-commit 31 | if: always() && fromJSON(needs.tag-bump-commit.outputs.did_tag || 'false') == false 32 | 33 | steps: 34 | - uses: actions/checkout@v4 35 | 36 | - name: Use Node.js 37 | uses: actions/setup-node@main 38 | with: 39 | node-version: 20.x 40 | 41 | - name: Update npm 42 | run: npm install -g npm@latest 43 | 44 | - name: npm install 45 | run: npm ci --workspaces 46 | 47 | - name: build packages 48 | run: npm run build --workspaces 49 | 50 | buildvtag: 51 | name: buildvtag 52 | runs-on: ubuntu-latest 53 | needs: tag-bump-commit 54 | if: always() && fromJSON(needs.tag-bump-commit.outputs.did_tag || 'false') == true 55 | steps: 56 | - uses: actions/checkout@v4 57 | 58 | - name: Use Node.js 59 | uses: actions/setup-node@main 60 | with: 61 | node-version: 20.x 62 | 63 | - name: Update npm 64 | run: npm install -g npm@latest 65 | 66 | - name: npm install 67 | run: npm ci --workspaces 68 | 69 | - name: build packages 70 | run: npm run build --workspaces 71 | 72 | - name: run publish script 73 | run: node ./scripts/release.js publish 74 | env: 75 | COMMIT_MESSAGE: ${{ github.event.head_commit.message }} -------------------------------------------------------------------------------- /packages/makecode-node/src/simserver.ts: -------------------------------------------------------------------------------- 1 | import http = require("http") 2 | import fs = require("fs") 3 | import { DownloadedEditor } from "makecode-core/built/mkc"; 4 | import { simloaderFiles } from "makecode-core/built/simloaderfiles"; 5 | 6 | const mime: pxt.Map = { 7 | js: "application/javascript", 8 | css: "text/css", 9 | html: "text/html", 10 | } 11 | 12 | export function startSimServer( 13 | ed: DownloadedEditor, 14 | port = 7001, 15 | forceLocal = false 16 | ) { 17 | http.createServer(async (request, response) => { 18 | let path = request.url 19 | if (path == "/") path = "/index.html" 20 | path = path.replace(/.*\//, "") 21 | path = path.replace(/\?.*/, "") 22 | 23 | let buf: Uint8Array = null 24 | 25 | if (path == "binary.js") { 26 | try { 27 | buf = fs.readFileSync("built/binary.js") 28 | } catch {} 29 | } else if (simloaderFiles.hasOwnProperty(path)) { 30 | if (forceLocal || path != "loader.js") 31 | try { 32 | buf = fs.readFileSync("assets/" + path) 33 | } catch { 34 | try { 35 | buf = fs.readFileSync("assets/js/" + path) 36 | } catch {} 37 | } 38 | if (!buf) buf = Buffer.from(simloaderFiles[path], "utf-8") 39 | } else if (/^[\w\.\-]+$/.test(path)) { 40 | buf = await ed.cache.getAsync(ed.website + "-" + path) 41 | if (!buf) buf = await ed.cache.getAsync(path) 42 | } 43 | 44 | if (buf) { 45 | const m = 46 | mime[path.replace(/.*\./, "")] || "application/octet-stream" 47 | response.writeHead(200, { 48 | "Content-type": m, 49 | "Cache-Control": "no-cache", 50 | }) 51 | response.end(buf) 52 | } else { 53 | response.writeHead(404, { "Content-type": "text/plain" }) 54 | response.end("Not found") 55 | } 56 | }).listen(port, "127.0.0.1") 57 | } 58 | -------------------------------------------------------------------------------- /packages/makecode-node/src/deploy.ts: -------------------------------------------------------------------------------- 1 | import * as child_process from "child_process" 2 | import * as util from "util" 3 | import * as fs from "fs" 4 | import * as path from "path" 5 | 6 | const cpExecAsync = util.promisify(child_process.exec) 7 | const readDirAsync = util.promisify(fs.readdir) 8 | 9 | function getBoardDrivesAsync(compile: any): Promise { 10 | if (process.platform == "win32") { 11 | const rx = new RegExp("^([A-Z]:)\\s+(\\d+).* " + compile.deployDrives) 12 | return cpExecAsync( 13 | "wmic PATH Win32_LogicalDisk get DeviceID, VolumeName, FileSystem, DriveType" 14 | ).then(({ stdout, stderr }) => { 15 | let res: string[] = [] 16 | stdout.split(/\n/).forEach(ln => { 17 | let m = rx.exec(ln) 18 | if (m && m[2] == "2") { 19 | res.push(m[1] + "/") 20 | } 21 | }) 22 | return res 23 | }) 24 | } else if (process.platform == "darwin") { 25 | const rx = new RegExp(compile.deployDrives) 26 | return readDirAsync("/Volumes").then(lst => 27 | lst.filter(s => rx.test(s)).map(s => "/Volumes/" + s + "/") 28 | ) 29 | } else if (process.platform == "linux") { 30 | const rx = new RegExp(compile.deployDrives) 31 | const user = process.env["USER"] 32 | if (fs.existsSync(`/media/${user}`)) 33 | return readDirAsync(`/media/${user}`).then(lst => 34 | lst.filter(s => rx.test(s)).map(s => `/media/${user}/${s}/`) 35 | ) 36 | return Promise.resolve([]) 37 | } else { 38 | return Promise.resolve([]) 39 | } 40 | } 41 | 42 | function filteredDrives(compile: any, drives: string[]): string[] { 43 | const marker = compile.deployFileMarker 44 | if (!marker) return drives 45 | return drives.filter(d => { 46 | try { 47 | return fs.existsSync(path.join(d, marker)) 48 | } catch (e) { 49 | return false 50 | } 51 | }) 52 | } 53 | 54 | export async function getDeployDrivesAsync(compile: any) { 55 | const drives = await getBoardDrivesAsync(compile) 56 | return filteredDrives(compile, drives) 57 | } 58 | -------------------------------------------------------------------------------- /.github/workflows/check-if-merged-pr.yml: -------------------------------------------------------------------------------- 1 | name: Check if the commit is part of a merged PR 2 | 3 | on: 4 | workflow_call: 5 | outputs: 6 | is_merged_pr: 7 | description: "Whether the current push came from a merged PR" 8 | value: ${{ jobs.check-pr.outputs.is_merged_pr }} 9 | pr_head_sha: 10 | description: "The head SHA of the merged PR" 11 | value: ${{ jobs.check-pr.outputs.pr_head_sha }} 12 | 13 | jobs: 14 | check-pr: 15 | runs-on: ubuntu-latest 16 | outputs: 17 | is_merged_pr: ${{ steps.parse-check-pr.outputs.is_merged_pr }} 18 | pr_head_sha: ${{ steps.parse-check-pr.outputs.pr_head_sha }} 19 | steps: 20 | - name: Check if this commit is from a merged PR 21 | id: check-pr 22 | uses: actions/github-script@v7 23 | with: 24 | result-encoding: string 25 | script: | 26 | const commitSha = context.sha; 27 | const { data: prs } = await github.rest.repos.listPullRequestsAssociatedWithCommit({ 28 | owner: context.repo.owner, 29 | repo: context.repo.repo, 30 | commit_sha: commitSha 31 | }); 32 | 33 | if (!prs.length) { 34 | core.info('No PRs associated with this commit.'); 35 | return JSON.stringify({ is_merged_pr: false, pr_head_sha: '' }); 36 | } 37 | 38 | const mergedPr = prs.find(pr => pr.merged_at !== null); 39 | 40 | if (!mergedPr) { 41 | core.info('PRs found, but none were merged.'); 42 | return JSON.stringify({ is_merged_pr: false, pr_head_sha: '' }); 43 | } 44 | 45 | core.info(`Found merged PR head SHA: ${mergedPr.head.sha}`); 46 | return JSON.stringify({ is_merged_pr: true, pr_head_sha: mergedPr.head.sha }); 47 | 48 | - name: Parse outputs 49 | id: parse-check-pr 50 | shell: bash 51 | run: | 52 | echo "Parsing result: ${{ steps.check-pr.outputs.result }}" 53 | echo "is_merged_pr=$(jq -r '.is_merged_pr' <<< '${{ steps.check-pr.outputs.result }}')" >> $GITHUB_OUTPUT 54 | echo "pr_head_sha=$(jq -r '.pr_head_sha' <<< '${{ steps.check-pr.outputs.result }}')" >> $GITHUB_OUTPUT 55 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Security 4 | 5 | Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/Microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet), [Xamarin](https://github.com/xamarin), and [many more](https://opensource.microsoft.com/). 6 | 7 | If you believe you have found a security vulnerability in any Microsoft-owned repository that meets Microsoft's [definition](https://docs.microsoft.com/en-us/previous-versions/tn-archive/cc751383(v=technet.10)) of a security vulnerability, please report it to us as described below. 8 | 9 | ## Reporting Security Issues 10 | 11 | **Please do not report security vulnerabilities through public GitHub issues.** 12 | 13 | Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://msrc.microsoft.com/create-report). 14 | 15 | If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the the [Microsoft Security Response Center PGP Key page](https://www.microsoft.com/en-us/msrc/pgp-key-msrc). 16 | 17 | You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://www.microsoft.com/msrc). 18 | 19 | Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue: 20 | 21 | * Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.) 22 | * Full paths of source file(s) related to the manifestation of the issue 23 | * The location of the affected source code (tag/branch/commit or direct URL) 24 | * Any special configuration required to reproduce the issue 25 | * Step-by-step instructions to reproduce the issue 26 | * Proof-of-concept or exploit code (if possible) 27 | * Impact of the issue, including how an attacker might exploit the issue 28 | 29 | This information will help us triage your report more quickly. 30 | 31 | If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://microsoft.com/msrc/bounty) page for more details about our active programs. 32 | 33 | ## Preferred Languages 34 | 35 | We prefer all communications to be in English. 36 | 37 | ## Policy 38 | 39 | Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://www.microsoft.com/en-us/msrc/cvd). 40 | 41 | 42 | -------------------------------------------------------------------------------- /.github/workflows/tag-bump-commit.yml: -------------------------------------------------------------------------------- 1 | name: Tag version on merged bump commit 2 | 3 | on: 4 | workflow_call: 5 | outputs: 6 | did_tag: 7 | description: 'Whether a tag was created' 8 | value: ${{ jobs.return.outputs.did_tag }} 9 | 10 | jobs: 11 | check-merge: 12 | uses: ./.github/workflows/check-if-merged-pr.yml 13 | 14 | check-merge-outputs: 15 | needs: check-merge 16 | runs-on: ubuntu-latest 17 | if: always() 18 | steps: 19 | - name: check-merge outputs 20 | run: | 21 | echo "is_merged_pr = '${{ needs.check-merge.outputs.is_merged_pr }}'" 22 | echo "pr_head_sha = '${{ needs.check-merge.outputs.pr_head_sha }}'" 23 | 24 | tag-version: 25 | needs: check-merge 26 | if: fromJSON(needs.check-merge.outputs.is_merged_pr || 'false') == true 27 | runs-on: ubuntu-latest 28 | outputs: 29 | did_tag: ${{ steps.tag-op.outputs.did_tag }} 30 | tag: ${{ steps.tag-op.outputs.tag }} 31 | steps: 32 | - uses: actions/checkout@v3 33 | with: 34 | fetch-depth: 0 35 | token: ${{ secrets.GITHUB_TOKEN }} 36 | 37 | - name: Tag commit if it's a version bump 38 | id: tag-op 39 | shell: bash 40 | run: | 41 | set -euxo pipefail 42 | 43 | COMMIT_SHA="${{ github.sha }}" 44 | echo "==> Current merge commit SHA: $COMMIT_SHA" 45 | 46 | echo "==> Fetching commit message..." 47 | COMMIT_MSG=$(git log -1 --pretty=%s "$COMMIT_SHA") 48 | echo "==> Commit message: '$COMMIT_MSG'" 49 | 50 | TAGGED=false 51 | 52 | # Check if commit matches bump pattern and PR# 53 | if [[ "$COMMIT_MSG" =~ \[release\]\ bump\ version\ to\ (makecode-core|makecode-browser|makecode)-v([0-9]+\.[0-9]+\.[0-9]+) ]]; then 54 | VERSION="${BASH_REMATCH[1]}-v${BASH_REMATCH[2]}" 55 | echo "==> Detected bump version: $VERSION" 56 | 57 | # Check if tag already exists 58 | if git rev-parse "$VERSION" >/dev/null 2>&1; then 59 | echo "::warning::Tag $VERSION already exists — skipping tagging." 60 | else 61 | echo "==> Tagging $COMMIT_SHA with $VERSION" 62 | git tag "$VERSION" "$COMMIT_SHA" 63 | git push origin "$VERSION" 64 | echo "tag=$VERSION" >> "$GITHUB_OUTPUT" 65 | TAGGED=true 66 | fi 67 | else 68 | echo "==> No merged bump commit detected — skipping tag creation." 69 | fi 70 | 71 | echo "==> did_tag=$TAGGED" 72 | echo "did_tag=$TAGGED" >> "$GITHUB_OUTPUT" 73 | 74 | not-tag-version: 75 | needs: check-merge 76 | if: fromJSON(needs.check-merge.outputs.is_merged_pr || 'false') == false 77 | runs-on: ubuntu-latest 78 | outputs: 79 | did_tag: false 80 | steps: 81 | - run: echo "No tag because not a PR merge." 82 | 83 | return: 84 | runs-on: ubuntu-latest 85 | needs: [tag-version, not-tag-version] 86 | if: always() 87 | outputs: 88 | did_tag: ${{ needs.tag-version.outputs.did_tag || false }} 89 | steps: 90 | - run: echo "Returning did_tag = ${{ needs.tag-version.outputs.did_tag || false }}" 91 | - run: echo "Returning tag = ${{ needs.tag-version.outputs.tag || '' }}" 92 | -------------------------------------------------------------------------------- /packages/makecode-core/src/host.ts: -------------------------------------------------------------------------------- 1 | import { WebConfig } from "./downloader"; 2 | import { DownloadedEditor, Package } from "./mkc"; 3 | import { BuiltSimJsInfo, CompileOptions, CompileResult } from "./service"; 4 | 5 | export interface Host { 6 | readFileAsync(path: string, encoding: "utf8"): Promise; 7 | readFileAsync(path: string, encoding?: "utf8"): Promise; 8 | 9 | writeFileAsync(path: string, content: any, encoding?: "base64" | "utf8"): Promise; 10 | mkdirAsync(path: string): Promise; 11 | rmdirAsync(path: string, options: any): Promise; 12 | existsAsync(path: string): Promise; 13 | unlinkAsync(path: string): Promise; 14 | symlinkAsync(target: string, path: string, type: "file"): Promise; 15 | listFilesAsync(directory: string, filename: string): Promise; 16 | requestAsync(options: HttpRequestOptions, validate?: (protocol: string, method: string) => void): Promise; 17 | createLanguageServiceAsync(editor: DownloadedEditor): Promise; 18 | getDeployDrivesAsync(compile: any): Promise; 19 | exitWithStatus(code: number): never; 20 | getEnvironmentVariable(key: string): string | undefined; 21 | cwdAsync(): Promise; 22 | 23 | bufferToString(buffer: Uint8Array): string; 24 | stringToBuffer (str: string, encoding?: "utf8" | "base64"): Uint8Array; 25 | base64EncodeBufferAsync(buffer: Uint8Array): Promise; 26 | 27 | guidGen?(): string; 28 | } 29 | 30 | export interface HttpRequestOptions { 31 | url: string 32 | method?: string // default to GET 33 | data?: any 34 | headers?: pxt.Map 35 | allowHttpErrors?: boolean // don't treat non-200 responses as errors 36 | allowGzipPost?: boolean 37 | } 38 | 39 | export interface HttpResponse { 40 | statusCode: number 41 | headers: pxt.Map 42 | buffer?: any 43 | text?: string 44 | json?: any 45 | } 46 | 47 | export interface SimpleDriverCallbacks { 48 | cacheGet: (key: string) => Promise 49 | cacheSet: (key: string, val: string) => Promise 50 | httpRequestAsync?: ( 51 | options: HttpRequestOptions 52 | ) => Promise 53 | pkgOverrideAsync?: (id: string) => Promise> 54 | } 55 | 56 | export interface LanguageService { 57 | registerDriverCallbacksAsync(callbacks: SimpleDriverCallbacks): Promise 58 | setWebConfigAsync(config: WebConfig): Promise; 59 | getWebConfigAsync(): Promise; 60 | getAppTargetAsync(): Promise; 61 | getTargetConfigAsync(): Promise; 62 | supportsGhPackagesAsync(): Promise; 63 | setHwVariantAsync(variant: string): Promise; 64 | getHardwareVariantsAsync(): Promise; 65 | getBundledPackageConfigsAsync(): Promise; 66 | getCompileOptionsAsync(prj: Package, simpleOpts?: any): Promise; 67 | installGhPackagesAsync(projectFiles: pxt.Map): Promise>; 68 | setProjectTextAsync(projectFiles: pxt.Map): Promise; 69 | performOperationAsync(op: string, options: any): Promise; 70 | 71 | enableExperimentalHardwareAsync(): Promise; 72 | enableDebugAsync(): Promise; 73 | setCompileSwitchesAsync(flags: string): Promise 74 | buildSimJsInfoAsync(result: CompileResult): Promise 75 | 76 | dispose?: () => void; 77 | } 78 | 79 | let host_: Host; 80 | 81 | export function setHost(newHost: Host) { 82 | host_ = newHost; 83 | } 84 | 85 | export function host() { 86 | if (!host) throw new Error("setHost() not called!") 87 | return host_; 88 | } -------------------------------------------------------------------------------- /packages/makecode-core/src/semver.ts: -------------------------------------------------------------------------------- 1 | export interface Version { 2 | major: number; 3 | minor: number; 4 | patch: number; 5 | pre: string[]; 6 | build: string[]; 7 | } 8 | 9 | export function cmp(a: Version, b: Version) { 10 | if (!a) 11 | if (!b) 12 | return 0; 13 | else 14 | return 1; 15 | else if (!b) 16 | return -1; 17 | else { 18 | let d = a.major - b.major || a.minor - b.minor || a.patch - b.patch 19 | if (d) return d 20 | if (a.pre.length == 0 && b.pre.length > 0) 21 | return 1; 22 | if (a.pre.length > 0 && b.pre.length == 0) 23 | return -1; 24 | for (let i = 0; i < a.pre.length + 1; ++i) { 25 | let aa = a.pre[i] 26 | let bb = b.pre[i] 27 | if (!aa) 28 | if (!bb) 29 | return 0; 30 | else 31 | return -1; 32 | else if (!bb) 33 | return 1; 34 | else if (/^\d+$/.test(aa)) 35 | if (/^\d+$/.test(bb)) { 36 | d = parseInt(aa) - parseInt(bb) 37 | if (d) return d 38 | } else return -1; 39 | else if (/^\d+$/.test(bb)) 40 | return 1 41 | else { 42 | d = strcmp(aa, bb) 43 | if (d) return d 44 | } 45 | } 46 | return 0 47 | } 48 | } 49 | 50 | export function parse(v: string, defaultVersion?: string): Version { 51 | let r = tryParse(v) || tryParse(defaultVersion) 52 | return r 53 | } 54 | 55 | export function tryParse(v: string): Version { 56 | if (!v) return null 57 | if ("*" === v) { 58 | return { 59 | major: Number.MAX_SAFE_INTEGER, 60 | minor: Number.MAX_SAFE_INTEGER, 61 | patch: Number.MAX_SAFE_INTEGER, 62 | pre: [], 63 | build: [] 64 | }; 65 | } 66 | if (/^v\d/i.test(v)) v = v.slice(1) 67 | let m = /^(\d+)\.(\d+)\.(\d+)(-([0-9a-zA-Z\-\.]+))?(\+([0-9a-zA-Z\-\.]+))?$/.exec(v) 68 | if (m) 69 | return { 70 | major: parseInt(m[1]), 71 | minor: parseInt(m[2]), 72 | patch: parseInt(m[3]), 73 | pre: m[5] ? m[5].split(".") : [], 74 | build: m[7] ? m[7].split(".") : [] 75 | } 76 | return null 77 | } 78 | 79 | export function normalize(v: string): string { 80 | return stringify(parse(v)); 81 | } 82 | 83 | export function stringify(v: Version) { 84 | let r = v.major + "." + v.minor + "." + v.patch 85 | if (v.pre.length) 86 | r += "-" + v.pre.join(".") 87 | if (v.build.length) 88 | r += "+" + v.build.join(".") 89 | return r 90 | } 91 | 92 | export function majorCmp(a: string, b: string) { 93 | let aa = tryParse(a) 94 | let bb = tryParse(b) 95 | return aa.major - bb.major; 96 | } 97 | 98 | /** 99 | * Compares two semver version strings and returns -1 if a < b, 1 if a > b and 0 100 | * if versions are equivalent. If a and b are invalid versions, classic strcmp is called. 101 | * If a (or b) is an invalid version, it is considered greater than any version (strmp(undefined, "0.0.0") = 1) 102 | */ 103 | export function compareStrings(a: string, b: string) { 104 | let aa = tryParse(a) 105 | let bb = tryParse(b) 106 | if (!aa && !bb) 107 | return strcmp(a, b) 108 | else return cmp(aa, bb) 109 | } 110 | 111 | export function inRange(rng: string, v: Version): boolean { 112 | let rngs = rng.split(' - '); 113 | if (rngs.length != 2) return false; 114 | let minInclusive = tryParse(rngs[0]); 115 | let maxExclusive = tryParse(rngs[1]); 116 | if (!minInclusive || !maxExclusive) return false; 117 | if (!v) return true; 118 | const lwr = cmp(minInclusive, v); 119 | const hr = cmp(v, maxExclusive); 120 | return lwr <= 0 && hr < 0; 121 | } 122 | 123 | /** 124 | * Filters and sort tags from latest to oldest (semver wize) 125 | * @param tags 126 | */ 127 | export function sortLatestTags(tags: string[]): string[] { 128 | const v = tags.filter(tag => !!tryParse(tag)); 129 | v.sort(compareStrings); 130 | v.reverse(); 131 | return v; 132 | } 133 | 134 | function strcmp(a: string, b: string) { 135 | if (a == b) return 0; 136 | if (a < b) return -1; 137 | else return 1; 138 | } -------------------------------------------------------------------------------- /packages/makecode-core/src/share.ts: -------------------------------------------------------------------------------- 1 | import * as path from "path" 2 | 3 | import { host } from "./host"; 4 | import { ProjectOptions, resolveProject } from "./commands"; 5 | import { WebConfig } from "./downloader"; 6 | import { Project } from "./mkc"; 7 | 8 | 9 | // This is copied from pxt. Should be kept up to date with that version 10 | interface InstallHeader { 11 | name: string; // script name, should always be in sync with pxt.json name 12 | meta: any; // script meta data 13 | editor: string; // editor that we're in 14 | board?: string; // name of the package that contains the board.json info 15 | temporary?: boolean; // don't serialize project 16 | // older script might miss this 17 | target: string; 18 | // older scripts might miss this 19 | targetVersion: string; 20 | pubId: string; // for published scripts 21 | pubCurrent: boolean; // is this exactly pubId, or just based on it 22 | // pubVersions?: PublishVersion[]; 23 | pubPermalink?: string; // permanent (persistent) share ID 24 | anonymousSharePreference?: boolean; // if true, default to sharing anonymously even when logged in 25 | githubId?: string; 26 | githubTag?: string; // the release tag if any (commit.tag) 27 | githubCurrent?: boolean; 28 | // workspace guid of the extension under test 29 | extensionUnderTest?: string; 30 | // id of cloud user who created this project 31 | cloudUserId?: string; 32 | isSkillmapProject?: boolean; 33 | 34 | id: string; // guid (generated by us) 35 | path?: string; // for workspaces that require it 36 | recentUse: number; // seconds since epoch 37 | modificationTime: number; // seconds since epoch 38 | icon?: string; // icon uri 39 | saveId?: any; // used to determine whether a project has been edited while we're saving to cloud 40 | pubVersions?: any[]; 41 | } 42 | 43 | const apiRoot = "https://www.makecode.com"; 44 | 45 | export async function shareProjectAsync(opts: ProjectOptions) { 46 | const prj = await resolveProject(opts); 47 | const req = await createShareLinkRequestAsync(prj); 48 | 49 | let siteRoot = new URL(prj.editor.website).origin; 50 | if (!siteRoot.endsWith("/")) { 51 | siteRoot += "/"; 52 | } 53 | 54 | const res = await host().requestAsync({ 55 | url: apiRoot + "/api/scripts", 56 | data: req 57 | }); 58 | 59 | if (res.statusCode === 200) { 60 | const resJSON = JSON.parse(res.text!) 61 | return siteRoot + resJSON.shortid 62 | } 63 | 64 | return undefined 65 | } 66 | 67 | async function createShareLinkRequestAsync(prj: Project) { 68 | const theHost = host(); 69 | 70 | const config = await prj.readPxtConfig(); 71 | 72 | const files: {[index: string]: string} = { 73 | "pxt.json": JSON.stringify(config) 74 | }; 75 | 76 | for (const file of config.files) { 77 | const content = await theHost.readFileAsync(path.join(prj.directory, file), "utf8"); 78 | files[file] = content; 79 | } 80 | 81 | if (config.testFiles) { 82 | for (const file of config.testFiles) { 83 | const content = await theHost.readFileAsync(path.join(prj.directory, file), "utf8"); 84 | files[file] = content; 85 | } 86 | } 87 | 88 | const target = await prj.service.languageService.getAppTargetAsync(); 89 | 90 | const header: InstallHeader = { 91 | "name": config.name, 92 | "meta": { 93 | "versions": target.versions 94 | }, 95 | "editor": "tsprj", 96 | "pubId": undefined, 97 | "pubCurrent": false, 98 | "target": target.id, 99 | "targetVersion": target.versions.target, 100 | "id": theHost.guidGen?.() || "", 101 | "recentUse": Date.now(), 102 | "modificationTime": Date.now(), 103 | "path": config.name, 104 | "saveId": {}, 105 | "githubCurrent": false, 106 | "pubVersions": [] 107 | } 108 | 109 | return { 110 | id: header.id, 111 | name: config.name, 112 | target: target.id, 113 | targetVersion: target.versions.target, 114 | description: config.description || `Made with ❤️ in MakeCode.`, 115 | editor: "tsprj", 116 | header: JSON.stringify(header), 117 | text: JSON.stringify(files), 118 | meta: { 119 | versions: target.versions 120 | } 121 | } 122 | } -------------------------------------------------------------------------------- /packages/makecode-core/simloader/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | MakeCode Simulator Driver 8 | 86 | 87 | 88 | 89 |
90 |
91 |
92 | 100 | 104 | 105 |
106 | 112 |
113 | 120 | 121 | 122 | -------------------------------------------------------------------------------- /packages/makecode-node/src/nodeHost.ts: -------------------------------------------------------------------------------- 1 | import { Host, HttpRequestOptions, HttpResponse } from "makecode-core/built/host"; 2 | import { glob } from "glob" 3 | import * as fs from "fs" 4 | import * as util from "util" 5 | import * as http from "http" 6 | import * as https from "https" 7 | import * as url from "url" 8 | import * as zlib from "zlib" 9 | import * as events from "events" 10 | import * as crypto from "crypto"; 11 | 12 | import { NodeLanguageService } from "./languageService"; 13 | import { getDeployDrivesAsync } from "./deploy"; 14 | 15 | export function createNodeHost(): Host { 16 | return { 17 | readFileAsync: util.promisify(fs.readFile), 18 | writeFileAsync: util.promisify(fs.writeFile), 19 | mkdirAsync: util.promisify(fs.mkdir), 20 | rmdirAsync: util.promisify(fs.rmdir), 21 | existsAsync: util.promisify(fs.exists), 22 | unlinkAsync: util.promisify(fs.unlink), 23 | symlinkAsync: util.promisify(fs.symlink), 24 | listFilesAsync: async (directory, filename) => 25 | glob.sync(directory + "/**/" + filename), 26 | requestAsync: nodeHttpRequestAsync, 27 | createLanguageServiceAsync: async (editor) => new NodeLanguageService(editor), 28 | getDeployDrivesAsync, 29 | getEnvironmentVariable: key => process.env[key], 30 | exitWithStatus: code => process.exit(code), 31 | cwdAsync: async () => process.cwd(), 32 | bufferToString: buffer => new util.TextDecoder("utf8").decode(buffer), 33 | stringToBuffer: (str, encoding) => Buffer.from(str, encoding), 34 | base64EncodeBufferAsync: async buffer => Buffer.isBuffer(buffer) ? buffer.toString("base64") : Buffer.from(buffer).toString("base64"), 35 | guidGen: () => crypto.randomUUID() 36 | } 37 | } 38 | 39 | function clone(v: T): T { 40 | if (!v) return v 41 | return JSON.parse(JSON.stringify(v)) 42 | } 43 | 44 | function nodeHttpRequestAsync( 45 | options: HttpRequestOptions, 46 | validate?: (protocol: string, method: string) => void 47 | ): Promise { 48 | let isHttps = false 49 | 50 | let u = (url.parse(options.url)) 51 | 52 | if (u.protocol == "https:") isHttps = true 53 | /* tslint:disable:no-http-string */ else if (u.protocol == "http:") 54 | isHttps = false 55 | /* tslint:enable:no-http-string */ else 56 | return Promise.reject("bad protocol: " + u.protocol) 57 | 58 | u.headers = clone(options.headers) || {} 59 | let data = options.data 60 | u.method = options.method || (data == null ? "GET" : "POST") 61 | 62 | if (validate) validate(u.protocol, u.method) 63 | 64 | let buf: Buffer = null 65 | 66 | u.headers["accept-encoding"] = "gzip" 67 | u.headers["user-agent"] = "MakeCode-CLI" 68 | 69 | let gzipContent = false 70 | 71 | if (data != null) { 72 | if (Buffer.isBuffer(data)) { 73 | buf = data 74 | } else if (typeof data == "object") { 75 | buf = Buffer.from(JSON.stringify(data), "utf8") 76 | u.headers["content-type"] = "application/json; charset=utf8" 77 | if (options.allowGzipPost) gzipContent = true 78 | } else if (typeof data == "string") { 79 | buf = Buffer.from(data, "utf8") 80 | if (options.allowGzipPost) gzipContent = true 81 | } else { 82 | throw new Error("bad data") 83 | } 84 | } 85 | 86 | if (gzipContent) { 87 | buf = zlib.gzipSync(buf) 88 | u.headers["content-encoding"] = "gzip" 89 | } 90 | 91 | if (buf) u.headers["content-length"] = buf.length 92 | 93 | return new Promise((resolve, reject) => { 94 | const handleResponse = (res: http.IncomingMessage) => { 95 | let g: events.EventEmitter = res 96 | if (/gzip/.test(res.headers["content-encoding"])) { 97 | let tmp = zlib.createUnzip() 98 | res.pipe(tmp) 99 | g = tmp 100 | } 101 | 102 | resolve( 103 | readResAsync(g).then(buf => { 104 | let text: string = null 105 | try { 106 | text = buf.toString("utf8") 107 | } catch (e) {} 108 | let resp: HttpResponse = { 109 | statusCode: res.statusCode, 110 | headers: res.headers, 111 | buffer: buf, 112 | text: text, 113 | } 114 | return resp 115 | }) 116 | ) 117 | } 118 | 119 | const req = isHttps 120 | ? https.request(u, handleResponse) 121 | : http.request(u, handleResponse) 122 | req.on("error", (err: any) => reject(err)) 123 | req.end(buf) 124 | }) 125 | } 126 | 127 | function readResAsync(g: events.EventEmitter) { 128 | return new Promise((resolve, reject) => { 129 | let bufs: Buffer[] = [] 130 | g.on("data", (c: any) => { 131 | if (typeof c === "string") bufs.push(Buffer.from(c, "utf8")) 132 | else bufs.push(c) 133 | }) 134 | 135 | g.on("error", (err: any) => reject(err)) 136 | 137 | g.on("end", () => resolve(Buffer.concat(bufs))) 138 | }) 139 | } 140 | 141 | -------------------------------------------------------------------------------- /packages/makecode-core/src/files.ts: -------------------------------------------------------------------------------- 1 | import * as path from "path" 2 | import * as mkc from "./mkc" 3 | import { host } from "./host"; 4 | import { cmp, stringify, tryParse } from "./semver"; 5 | 6 | export async function findParentDirWithAsync(base: string, filename: string) { 7 | let s = base 8 | while (true) { 9 | if (await host().existsAsync(path.join(s, filename))) return s 10 | 11 | const s2 = path.resolve(path.join(s, "..")) 12 | if (s == s2) return null 13 | s = s2 14 | } 15 | } 16 | 17 | export async function findProjectDirAsync() { 18 | return findParentDirWithAsync(await host().cwdAsync(), "pxt.json") 19 | } 20 | 21 | function resolveFilename(dir: string, filename: string) { 22 | const resolved = path.resolve(dir, filename) 23 | if (resolved.startsWith(path.resolve(".", dir))) return resolved 24 | throw new Error(`Invalid file name: ${filename} (in ${dir})`) 25 | } 26 | 27 | export function relativePath(currdir: string, target: string) { 28 | return path.relative(currdir, target) 29 | } 30 | 31 | export function fileExistsAsync(name: string) { 32 | return host().existsAsync(name) 33 | } 34 | 35 | export function readPrjFileAsync(dir: string, filename: string) { 36 | return host().readFileAsync(resolveFilename(dir, filename), "utf8") 37 | } 38 | 39 | export async function readProjectAsync(dir: string) { 40 | const pxtJson = await host().readFileAsync(path.join(dir, "pxt.json"), "utf8") 41 | const res: mkc.Package = { 42 | config: JSON.parse(pxtJson), 43 | mkcConfig: null, // JSON.parse(await readAsync(path.join(dir, "mkc.json"), "utf8").then(s => s, err => "{}")), 44 | files: { 45 | "pxt.json": pxtJson, 46 | }, 47 | } 48 | for (const fn of res.config.files.concat(res.config.testFiles || [])) { 49 | res.files[fn] = await host().readFileAsync(resolveFilename(dir, fn), "utf8") 50 | } 51 | return res.files 52 | } 53 | 54 | function homePxtDir() { 55 | return path.join(host().getEnvironmentVariable("HOME") || host().getEnvironmentVariable("UserProfile"), ".pxt") 56 | } 57 | 58 | export async function mkHomeCacheAsync(dir?: string): Promise { 59 | if (!dir) dir = homePxtDir() 60 | await mkdirpAsync(dir) 61 | const rootPath = path.join(dir, "mkc-cache") 62 | await mkdirpAsync(rootPath) 63 | 64 | function expandKey(key: string) { 65 | return key.replace(/[^\.a-z0-9_\-]/g, c => "_" + c.charCodeAt(0) + "_") 66 | } 67 | 68 | function keyPath(key: string) { 69 | return path.join(rootPath, expandKey(key)) 70 | } 71 | 72 | return { 73 | rootPath, 74 | expandKey, 75 | getAsync: key => 76 | host().readFileAsync(keyPath(key)).then( 77 | buf => buf, 78 | err => null 79 | ), 80 | setAsync: (key, val) => host().writeFileAsync(keyPath(key), val), 81 | } 82 | } 83 | 84 | async function mkdirpAsync(dirname: string, lev = 5) { 85 | if (!await host().existsAsync(dirname)) { 86 | if (lev > 0) await mkdirpAsync(path.resolve(dirname, ".."), lev - 1) 87 | await host().mkdirAsync(dirname) 88 | } 89 | } 90 | 91 | export async function writeFilesAsync( 92 | built: string, 93 | outfiles: pxt.Map, 94 | log = false 95 | ) { 96 | await mkdirpAsync(built) 97 | for (let fn of Object.keys(outfiles)) { 98 | if (fn.indexOf("/") >= 0) continue 99 | if (log) mkc.log(`write ${built}/${fn}`) 100 | if (/\.(uf2|pxt64|elf)$/.test(fn)) 101 | await host().writeFileAsync(path.join(built, fn), outfiles[fn], "base64") 102 | else await host().writeFileAsync(path.join(built, fn), outfiles[fn], "utf8") 103 | } 104 | } 105 | 106 | export async function saveBuiltFilesAsync( 107 | dir: string, 108 | res: mkc.service.CompileResult, 109 | folder = "built" 110 | ) { 111 | await writeFilesAsync(path.join(dir, folder), res.outfiles || {}, true) 112 | } 113 | 114 | export async function savePxtModulesAsync( 115 | dir: string, 116 | files: pxt.Map 117 | ) { 118 | for (const k of Object.keys(files)) 119 | if (k.startsWith("pxt_modules/")) { 120 | await mkdirpAsync(path.dirname(k)) 121 | const v = files[k] 122 | if (typeof v == "string") { 123 | mkc.debug(` write ${k}`) 124 | await host().writeFileAsync(k, v) 125 | } 126 | else { 127 | mkc.debug(` link ${k}`) 128 | try { 129 | await host().unlinkAsync(k) 130 | } catch { } 131 | await host().symlinkAsync(v.symlink, k, "file") 132 | } 133 | } 134 | } 135 | 136 | export async function monoRepoConfigsAsync(folder: string, includingSelf = true) { 137 | const files = await host().listFilesAsync(folder, "pxt.json"); 138 | return files.filter( 139 | e => 140 | e.indexOf("pxt_modules") < 0 && 141 | e.indexOf("node_modules") < 0 && 142 | (includingSelf || 143 | path.resolve(folder, "pxt.json") != path.resolve(e)) 144 | ) 145 | } 146 | 147 | export async function collectCurrentVersionAsync(configs: string[]) { 148 | let version = tryParse("0.0.0") 149 | for (const config of configs) { 150 | const cfg = JSON.parse(await host().readFileAsync(config, "utf8")) 151 | const v = tryParse(cfg.version); 152 | if (v && cmp(version, v) < 0) 153 | version = v 154 | } 155 | return stringify(version); 156 | } -------------------------------------------------------------------------------- /packages/makecode-browser/src/types.d.ts: -------------------------------------------------------------------------------- 1 | interface WorkerMessage { 2 | id?: number; 3 | } 4 | 5 | interface BaseResponse extends WorkerMessage { 6 | response: true; 7 | } 8 | 9 | interface RegisterDriverCallbacksRequest extends WorkerMessage { 10 | type: "registerDriverCallbacks"; 11 | } 12 | 13 | interface RegisterDriverCallbacksResponse extends BaseResponse { 14 | type: "registerDriverCallbacks"; 15 | } 16 | 17 | interface SetWebConfigRequest extends WorkerMessage { 18 | type: "setWebConfig"; 19 | webConfig: any; 20 | } 21 | 22 | interface SetWebConfigResponse extends BaseResponse { 23 | type: "setWebConfig"; 24 | } 25 | 26 | interface GetWebConfigRequest extends WorkerMessage { 27 | type: "getWebConfig"; 28 | } 29 | 30 | interface GetWebConfigResponse extends BaseResponse { 31 | type: "getWebConfig"; 32 | webConfig: any; 33 | } 34 | 35 | interface GetAppTargetRequest extends WorkerMessage { 36 | type: "getAppTarget"; 37 | } 38 | 39 | interface GetAppTargetResponse extends BaseResponse { 40 | type: "getAppTarget"; 41 | appTarget: any; 42 | } 43 | 44 | interface SupportsGhPackagesRequest extends WorkerMessage { 45 | type: "supportsGhPackages"; 46 | } 47 | 48 | interface SupportsGhPackagesResponse extends BaseResponse { 49 | type: "supportsGhPackages"; 50 | supported: boolean; 51 | } 52 | 53 | interface SetHwVariantRequest extends WorkerMessage { 54 | type: "setHwVariant"; 55 | variant: string; 56 | } 57 | 58 | interface SetHwVariantResponse extends BaseResponse { 59 | type: "setHwVariant"; 60 | } 61 | 62 | interface GetHardwareVariantsRequest extends WorkerMessage { 63 | type: "getHardwareVariants"; 64 | } 65 | 66 | interface GetHardwareVariantsResponse extends BaseResponse { 67 | type: "getHardwareVariants"; 68 | configs: any[]; 69 | } 70 | 71 | interface GetBundledPackageConfigsRequest extends WorkerMessage { 72 | type: "getBundledPackageConfigs"; 73 | } 74 | 75 | interface GetBundledPackageConfigsResponse extends BaseResponse { 76 | type: "getBundledPackageConfigs"; 77 | configs: any[]; 78 | } 79 | 80 | interface GetCompileOptionsAsyncRequest extends WorkerMessage { 81 | type: "getCompileOptionsAsync"; 82 | opts: any; 83 | } 84 | 85 | interface GetCompileOptionsAsyncResponse extends BaseResponse { 86 | type: "getCompileOptionsAsync"; 87 | result: any; 88 | } 89 | 90 | interface InstallGhPackagesAsyncRequest extends WorkerMessage { 91 | type: "installGhPackagesAsync"; 92 | files: pxt.Map; 93 | } 94 | 95 | interface InstallGhPackagesAsyncResponse extends BaseResponse { 96 | type: "installGhPackagesAsync"; 97 | result: pxt.Map; 98 | } 99 | 100 | interface PerformOperationRequest extends WorkerMessage { 101 | type: "performOperation"; 102 | op: string; 103 | data: any; 104 | } 105 | 106 | interface PerformOperationResponse extends BaseResponse { 107 | type: "performOperation"; 108 | result: any; 109 | } 110 | 111 | interface SetProjectTextRequest extends WorkerMessage { 112 | type: "setProjectText"; 113 | files: pxt.Map; 114 | } 115 | 116 | interface SetProjectTextResponse extends BaseResponse { 117 | type: "setProjectText"; 118 | } 119 | 120 | interface EnableExperimentalHardwareRequest extends WorkerMessage { 121 | type: "enableExperimentalHardware"; 122 | } 123 | 124 | interface EnableExperimentalHardwareResponse extends BaseResponse { 125 | type: "enableExperimentalHardware"; 126 | } 127 | 128 | interface EnableDebugRequest extends WorkerMessage { 129 | type: "enableDebug"; 130 | } 131 | 132 | interface EnableDebugResponse extends BaseResponse { 133 | type: "enableDebug"; 134 | } 135 | 136 | interface SetCompileSwitchesRequest extends WorkerMessage { 137 | type: "setCompileSwitches"; 138 | flags: string; 139 | } 140 | 141 | interface SetCompileSwitchesResponse extends BaseResponse { 142 | type: "setCompileSwitches"; 143 | } 144 | 145 | type ClientToWorkerRequest = 146 | | RegisterDriverCallbacksRequest 147 | | SetWebConfigRequest 148 | | GetWebConfigRequest 149 | | GetAppTargetRequest 150 | | SupportsGhPackagesRequest 151 | | SetHwVariantRequest 152 | | GetHardwareVariantsRequest 153 | | GetBundledPackageConfigsRequest 154 | | GetCompileOptionsAsyncRequest 155 | | InstallGhPackagesAsyncRequest 156 | | PerformOperationRequest 157 | | SetProjectTextRequest 158 | | EnableExperimentalHardwareRequest 159 | | EnableDebugRequest 160 | | SetCompileSwitchesRequest 161 | 162 | type ClientToWorkerRequestResponse = 163 | | RegisterDriverCallbacksResponse 164 | | SetWebConfigResponse 165 | | GetWebConfigResponse 166 | | GetAppTargetResponse 167 | | SupportsGhPackagesResponse 168 | | SetHwVariantResponse 169 | | GetHardwareVariantsResponse 170 | | GetBundledPackageConfigsResponse 171 | | GetCompileOptionsAsyncResponse 172 | | InstallGhPackagesAsyncResponse 173 | | PerformOperationResponse 174 | | SetProjectTextResponse 175 | | EnableExperimentalHardwareResponse 176 | | EnableDebugResponse 177 | | SetCompileSwitchesResponse 178 | 179 | interface BaseWorkerToClientRequest extends WorkerMessage { 180 | kind: "worker-to-client"; 181 | } 182 | 183 | interface BaseWorkerToClientRequestResponse extends BaseWorkerToClientRequest { 184 | response: true; 185 | } 186 | 187 | interface CacheSetRequest extends BaseWorkerToClientRequest { 188 | type: "cacheSet"; 189 | key: string; 190 | value: string; 191 | } 192 | 193 | interface CacheSetResponse extends BaseWorkerToClientRequestResponse { 194 | type: "cacheSet"; 195 | } 196 | 197 | interface CacheGetRequest extends BaseWorkerToClientRequest { 198 | type: "cacheGet"; 199 | key: string; 200 | } 201 | 202 | interface CacheGetResponse extends BaseWorkerToClientRequestResponse { 203 | type: "cacheGet"; 204 | value: string; 205 | } 206 | 207 | interface PackageOverrideRequest extends BaseWorkerToClientRequest { 208 | type: "packageOverride"; 209 | packageId: string; 210 | } 211 | 212 | interface PackageOverrideResponse extends BaseWorkerToClientRequestResponse { 213 | type: "packageOverride"; 214 | files: pxt.Map; 215 | } 216 | 217 | type WorkerToClientRequest = CacheSetRequest | CacheGetRequest | PackageOverrideRequest; 218 | 219 | type WorkerToClientRequestResponse = CacheSetResponse | CacheGetResponse | PackageOverrideResponse; -------------------------------------------------------------------------------- /packages/makecode-node/src/bump.ts: -------------------------------------------------------------------------------- 1 | import * as child_process from "child_process" 2 | import * as fs from "fs" 3 | import * as mkc from "makecode-core/built/mkc" 4 | import { httpGetJsonAsync } from "makecode-core/built/downloader" 5 | import { inc } from "semver" 6 | import { collectCurrentVersionAsync, monoRepoConfigsAsync } from "makecode-core/built/files" 7 | 8 | export interface SpawnOptions { 9 | cmd: string 10 | args: string[] 11 | cwd?: string 12 | shell?: boolean 13 | pipe?: boolean 14 | input?: string 15 | silent?: boolean 16 | allowNonZeroExit?: boolean 17 | } 18 | 19 | export function spawnAsync(opts: SpawnOptions) { 20 | opts.pipe = false 21 | return spawnWithPipeAsync(opts).then(() => {}) 22 | } 23 | 24 | export function spawnWithPipeAsync(opts: SpawnOptions) { 25 | if (opts.pipe === undefined) opts.pipe = true 26 | let info = opts.cmd + " " + opts.args.join(" ") 27 | if (opts.cwd && opts.cwd != ".") info = "cd " + opts.cwd + "; " + info 28 | mkc.log("[run] " + info) 29 | return new Promise((resolve, reject) => { 30 | let ch = child_process.spawn(opts.cmd, opts.args, { 31 | cwd: opts.cwd, 32 | env: process.env, 33 | stdio: opts.pipe 34 | ? [ 35 | opts.input == null ? process.stdin : "pipe", 36 | "pipe", 37 | process.stderr, 38 | ] 39 | : "inherit", 40 | shell: opts.shell || false, 41 | } as any) 42 | let bufs: Buffer[] = [] 43 | if (opts.pipe) 44 | ch.stdout.on("data", (buf: Buffer) => { 45 | bufs.push(buf) 46 | if (!opts.silent) { 47 | process.stdout.write(buf) 48 | } 49 | }) 50 | ch.on("close", (code: number) => { 51 | if (code != 0 && !opts.allowNonZeroExit) 52 | reject(new Error("Exit code: " + code + " from " + info)) 53 | resolve(Buffer.concat(bufs)) 54 | }) 55 | if (opts.input != null) ch.stdin.end(opts.input, "utf8") 56 | }) 57 | } 58 | 59 | let readlineCount = 0 60 | function readlineAsync() { 61 | process.stdin.resume() 62 | process.stdin.setEncoding("utf8") 63 | readlineCount++ 64 | return new Promise((resolve, reject) => { 65 | process.stdin.once("data", (text: string) => { 66 | process.stdin.pause(); 67 | resolve(text) 68 | }) 69 | }) 70 | } 71 | 72 | export function queryAsync(msg: string, defl: string) { 73 | process.stdout.write(`${msg} [${defl}]: `) 74 | return readlineAsync().then(text => { 75 | text = text.trim() 76 | if (!text) return defl 77 | else return text 78 | }) 79 | } 80 | 81 | export function needsGitCleanAsync() { 82 | return Promise.resolve() 83 | .then(() => 84 | spawnWithPipeAsync({ 85 | cmd: "git", 86 | args: ["status", "--porcelain", "--untracked-files=no"], 87 | }) 88 | ) 89 | .then(buf => { 90 | if (buf.length) 91 | throw new Error( 92 | "Please commit all files to git before running 'makecode --bump'" 93 | ) 94 | }) 95 | } 96 | 97 | export function runGitAsync(...args: string[]) { 98 | return spawnAsync({ 99 | cmd: "git", 100 | args: args, 101 | cwd: ".", 102 | }) 103 | } 104 | 105 | export async function bumpAsync( 106 | prj: mkc.Project, 107 | versionFile: string, 108 | stage: boolean, 109 | release: "patch" | "minor" | "major" 110 | ) { 111 | if (stage) mkc.log(`operation staged, skipping git commit/push`) 112 | 113 | if (!stage) { 114 | await needsGitCleanAsync() 115 | await runGitAsync("pull") 116 | } 117 | const configs = await monoRepoConfigsAsync(prj.directory, true) 118 | const currentVersion = await collectCurrentVersionAsync(configs) 119 | let newV: string 120 | if (release) 121 | newV = inc(currentVersion, release) 122 | else 123 | newV = await queryAsync("New version", inc(currentVersion, "patch")) 124 | const newTag = "v" + newV 125 | mkc.log(`new version: ${newV}`) 126 | 127 | if (versionFile) { 128 | const cfg = prj.mainPkg.config 129 | mkc.debug(`writing version in ${versionFile}`) 130 | const versionSrc = ` 131 | // Auto-generated file: do not edit. 132 | namespace ${cfg.name 133 | .replace(/^pxt-/, "") 134 | .split(/-/g) 135 | .map((p, i) => (i == 0 ? p : p[0].toUpperCase() + p.slice(1))) 136 | .join("")} { 137 | /** 138 | * Version of the package 139 | */ 140 | export const VERSION = "${newTag}" 141 | }` 142 | fs.writeFileSync(versionFile, versionSrc, { encoding: "utf-8" }) 143 | } 144 | 145 | for (const fn of configs) { 146 | const cfg0 = JSON.parse(fs.readFileSync(fn, "utf8")) 147 | if (cfg0?.codal?.libraries?.length == 1) { 148 | const lib: string = cfg0.codal.libraries[0] 149 | if (lib.endsWith("#v" + cfg0.version)) { 150 | mkc.debug(`updating codal library in ${fn}`) 151 | cfg0.codal.libraries[0] = lib.replace(/#.*/, "#v" + newV) 152 | } 153 | } 154 | cfg0.version = newV 155 | mkc.debug(`updating ${fn}`) 156 | fs.writeFileSync(fn, mkc.stringifyConfig(cfg0)) 157 | } 158 | 159 | if (!stage) { 160 | await runGitAsync("commit", "-a", "-m", newV) 161 | await runGitAsync("tag", newTag) 162 | await runGitAsync("push") 163 | await runGitAsync("push", "--tags") 164 | 165 | const urlinfo = await spawnWithPipeAsync({ 166 | cmd: "git", 167 | args: ["remote", "get-url", "origin"], 168 | pipe: true, 169 | }).then( 170 | v => v, 171 | err => { 172 | mkc.error(err) 173 | return null as Buffer 174 | } 175 | ) 176 | const url = urlinfo?.toString("utf8")?.trim() 177 | if (url) { 178 | const slug = url.replace(/.*github\.com\//i, "") 179 | if (slug != url) { 180 | mkc.log(`Github slug ${slug}; refreshing makecode.com cache`) 181 | const res = await httpGetJsonAsync( 182 | "https://makecode.com/api/gh/" + slug + "/refs?nocache=1" 183 | ) 184 | const sha = res?.refs?.["refs/tags/" + newTag] 185 | mkc.debug(`refreshed ${newV} -> ${sha}`) 186 | } 187 | } 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /packages/makecode-core/src/loader.ts: -------------------------------------------------------------------------------- 1 | import * as mkc from "./mkc" 2 | import * as downloader from "./downloader" 3 | import { host } from "./host" 4 | 5 | export interface TargetDescriptor { 6 | id: string 7 | targetId: string 8 | name: string 9 | description: string 10 | website: string 11 | corepkg?: string 12 | label?: string 13 | dependencies?: Record 14 | testDependencies?: Record 15 | } 16 | 17 | export const descriptors: TargetDescriptor[] = [ 18 | { 19 | id: "arcade", 20 | targetId: "arcade", 21 | name: "MakeCode Arcade", 22 | description: "Old school games", 23 | website: "https://arcade.makecode.com/", 24 | corepkg: "device", 25 | }, 26 | { 27 | id: "microbit", 28 | targetId: "microbit", 29 | name: "micro:bit", 30 | description: "Get creative, get connected, get coding", 31 | website: "https://makecode.microbit.org/beta", 32 | corepkg: "core", 33 | dependencies: { 34 | core: "*", 35 | radio: "*", 36 | microphone: "*", 37 | }, 38 | }, 39 | { 40 | id: "maker-jacdac-brain-esp32", 41 | targetId: "maker", 42 | name: "Maker ESP32-S2", 43 | description: "Jacdac ESP32-S2 brain", 44 | website: "https://maker.makecode.com/", 45 | corepkg: "jacdac-iot-s2", 46 | }, 47 | { 48 | id: "maker-jacdac-brain-f4", 49 | targetId: "maker", 50 | name: "Maker Jacdac Brain F4", 51 | description: "Jacdac STM32 F4 brain", 52 | website: "https://maker.makecode.com/", 53 | corepkg: "jacdac-brain-f4", 54 | }, 55 | { 56 | id: "maker-jacdac-brain-rp2040", 57 | targetId: "maker", 58 | name: "Maker Jacdac Brain RP2040", 59 | description: "Jacdac STM32 RP2040 brain", 60 | website: "https://maker.makecode.com/", 61 | corepkg: "jacdac-brain-rp2040", 62 | }, 63 | { 64 | id: "maker-jacdac-brain-nrf52", 65 | targetId: "maker", 66 | name: "Maker Jacdac Brain NRF52", 67 | description: "Jacdac STM32 NRF52 brain", 68 | website: "https://maker.makecode.com/", 69 | corepkg: "jacdac-nrfbrain", 70 | }, 71 | { 72 | id: "adafruit", 73 | targetId: "adafruit", 74 | name: "Circuit Playground Express", 75 | description: "An educational board from Adafruit", 76 | website: "https://makecode.adafruit.com/beta", 77 | corepkg: "circuit-playground", 78 | }, 79 | ] 80 | 81 | export function guessMkcJson(prj: mkc.Package) { 82 | const mkc = prj.mkcConfig 83 | const ver = prj.config.targetVersions || { target: "" } 84 | const vers = prj.config.supportedTargets || [] 85 | 86 | const theTarget = 87 | descriptors.find(d => d.targetId == ver.targetId) || 88 | descriptors.find(d => d.website == ver.targetWebsite) || 89 | descriptors.find(d => vers.indexOf(d.targetId) > -1) || 90 | descriptors.find( 91 | d => 92 | (d.corepkg && !!prj.config?.testDependencies?.[d.corepkg]) || 93 | !!prj.config.dependencies[d.corepkg] 94 | ) 95 | 96 | if (!mkc.targetWebsite) { 97 | if (ver.targetWebsite) { 98 | mkc.targetWebsite = ver.targetWebsite 99 | } else if (theTarget) { 100 | mkc.targetWebsite = theTarget.website 101 | } else { 102 | throw new Error( 103 | "Cannot determine target; please use mkc.json to specify" 104 | ) 105 | } 106 | } 107 | } 108 | 109 | function merge(trg: any, src: any) { 110 | for (const k of Object.keys(src)) trg[k] = src[k] 111 | } 112 | 113 | async function recLoadAsync( 114 | ed: mkc.DownloadedEditor, 115 | ws: mkc.Workspace, 116 | myid = "this" 117 | ) { 118 | const mkcJson = ws.packages["this"].mkcConfig 119 | const pcfg = ws.packages[myid].config 120 | const pending: string[] = [] 121 | let deps = pcfg.dependencies 122 | if (myid == "this" && pcfg.testDependencies) { 123 | deps = {} 124 | merge(deps, pcfg.dependencies) 125 | merge(deps, pcfg.testDependencies) 126 | } 127 | for (let pkgid of Object.keys(deps)) { 128 | const ver = deps[pkgid] 129 | if (pkgid == "hw" && mkcJson.hwVariant) 130 | pkgid = "hw---" + mkcJson.hwVariant 131 | if (ws.packages[pkgid] !== undefined) continue // already loaded 132 | let text: pxt.Map 133 | let fromTargetJson = false 134 | pending.push(pkgid) 135 | if (mkcJson.links && mkcJson.links[pkgid]) { 136 | text = await mkc.files.readProjectAsync(mkcJson.links[pkgid]) 137 | } else if (ver == "*" || /^file:/.test(ver)) { 138 | text = ed.targetJson.bundledpkgs[pkgid] 139 | if (!text) 140 | throw new Error(`Package ${pkgid} not found in target.json`) 141 | fromTargetJson = true 142 | } else { 143 | let m = /^github:([\w\-\.]+\/[\w\-\.]+)#([\w\-\.]+)$/.exec(ver) 144 | if (m) { 145 | const path = m[1] + "/" + m[2] 146 | let curr = await ed.cache.getAsync("gh-" + path) 147 | if (!curr) { 148 | const res = await downloader.requestAsync({ 149 | url: mkc.cloudRoot + "gh/" + path + "/text", 150 | }) 151 | curr = res.buffer 152 | await ed.cache.setAsync("gh-" + path, curr) 153 | } 154 | text = JSON.parse(host().bufferToString(curr)) 155 | } else { 156 | throw new Error(`Unsupported package version: ${pkgid}: ${ver}`) 157 | } 158 | } 159 | const pkg: mkc.Package = { 160 | config: JSON.parse(text["pxt.json"]), 161 | mkcConfig: null, 162 | files: text, 163 | fromTargetJson, 164 | } 165 | ws.packages[pkgid] = pkg 166 | ws.packages[pkgid.replace(/---.*/, "")] = pkg 167 | } 168 | 169 | for (let id of pending) await recLoadAsync(ed, ws, id) 170 | } 171 | 172 | export async function loadDeps(ed: mkc.DownloadedEditor, mainPrj: mkc.Package) { 173 | const ws: mkc.Workspace = { 174 | packages: { 175 | this: mainPrj, 176 | }, 177 | } 178 | 179 | await recLoadAsync(ed, ws) 180 | 181 | for (let k of Object.keys(ws.packages)) { 182 | if (k == "this") continue 183 | const prj = ws.packages[k] 184 | for (let fn of Object.keys(prj.files)) 185 | mainPrj.files["pxt_modules/" + k + "/" + fn] = prj.files[fn] 186 | } 187 | 188 | // console.log(Object.keys(mainPrj.files)) 189 | } 190 | -------------------------------------------------------------------------------- /packages/makecode-node/src/languageService.ts: -------------------------------------------------------------------------------- 1 | import * as vm from "vm"; 2 | import * as util from "util"; 3 | 4 | import * as mkc from "makecode-core/built/mkc"; 5 | import { WebConfig } from "makecode-core/built/downloader"; 6 | import { BuiltSimJsInfo, CompileOptions, CompileResult } from "makecode-core/built/service"; 7 | import { LanguageService, SimpleDriverCallbacks } from "makecode-core/built/host"; 8 | 9 | export class NodeLanguageService implements LanguageService { 10 | sandbox: vm.Context 11 | 12 | constructor(public editor: mkc.DownloadedEditor) { 13 | this.sandbox = { 14 | eval: (str: string) => 15 | vm.runInContext(str, this.sandbox, { 16 | filename: "eval", 17 | }), 18 | Function: undefined, 19 | setTimeout: setTimeout, 20 | clearInterval: clearInterval, 21 | clearTimeout: clearTimeout, 22 | setInterval: setInterval, 23 | clearImmediate: clearImmediate, 24 | setImmediate: setImmediate, 25 | TextEncoder: util.TextEncoder, 26 | TextDecoder: util.TextDecoder, 27 | Buffer: Buffer, 28 | pxtTargetBundle: {}, 29 | scriptText: {}, 30 | global: null, 31 | console: { 32 | log: (s: string) => mkc.log(s), 33 | debug: (s: string) => mkc.debug(s), 34 | warn: (s: string) => mkc.error(s), 35 | }, 36 | }; 37 | 38 | this.sandbox.global = this.sandbox 39 | vm.createContext(this.sandbox, { 40 | codeGeneration: { 41 | strings: false, 42 | wasm: false, 43 | }, 44 | }); 45 | 46 | const ed = this.editor 47 | ed.targetJson.compile.keepCppFiles = true 48 | this.sandbox.pxtTargetBundle = ed.targetJson 49 | this.runScript(ed.pxtWorkerJs, ed.website + "/pxtworker.js") 50 | } 51 | 52 | async registerDriverCallbacksAsync(callbacks: SimpleDriverCallbacks): Promise { 53 | this.runFunctionSync("pxt.setupSimpleCompile", [callbacks]); 54 | // disable packages config for now; 55 | // otherwise we do a HTTP request on every compile 56 | this.runSync( 57 | "pxt.packagesConfigAsync = () => Promise.resolve({})" 58 | ) 59 | } 60 | 61 | async setWebConfigAsync(config: WebConfig): Promise { 62 | this.runFunctionSync("pxt.setupWebConfig", [ 63 | config 64 | ]) 65 | } 66 | 67 | async getWebConfigAsync(): Promise { 68 | return this.runSync("pxt.webConfig") 69 | } 70 | 71 | async getAppTargetAsync(): Promise { 72 | return this.runSync("pxt.appTarget"); 73 | } 74 | 75 | async getTargetConfigAsync(): Promise { 76 | return this.editor.targetConfig; 77 | } 78 | 79 | async supportsGhPackagesAsync(): Promise { 80 | return !!this.runSync("pxt.simpleInstallPackagesAsync") 81 | } 82 | 83 | async setHwVariantAsync(variant: string): Promise { 84 | this.runFunctionSync("pxt.setHwVariant", [ 85 | variant || "", 86 | ]) 87 | } 88 | 89 | async getHardwareVariantsAsync(): Promise { 90 | return this.runSync( 91 | "pxt.getHwVariants()" 92 | ) 93 | } 94 | 95 | async getBundledPackageConfigsAsync(): Promise { 96 | return this.runSync( 97 | "Object.values(pxt.appTarget.bundledpkgs).map(pkg => JSON.parse(pkg['pxt.json']))" 98 | ); 99 | } 100 | 101 | async getCompileOptionsAsync(prj: mkc.Package, simpleOpts?: any): Promise { 102 | this.sandbox._opts = simpleOpts 103 | return this.runAsync( 104 | "pxt.simpleGetCompileOptionsAsync(_scriptText, _opts)" 105 | ) 106 | } 107 | 108 | async installGhPackagesAsync(projectFiles: pxt.Map): Promise> { 109 | await this.runFunctionAsync("pxt.simpleInstallPackagesAsync", [ 110 | projectFiles, 111 | ]) 112 | return projectFiles; 113 | } 114 | 115 | performOperationAsync(op: string, data: any): Promise { 116 | return this.runFunctionSync("pxtc.service.performOperation", [op, data]) 117 | } 118 | 119 | async setProjectTextAsync(projectFiles: pxt.Map): Promise { 120 | this.sandbox._scriptText = projectFiles; 121 | } 122 | 123 | async enableExperimentalHardwareAsync(): Promise { 124 | this.runSync( 125 | "(() => { pxt.savedAppTheme().experimentalHw = true; pxt.reloadAppTargetVariant() })()" 126 | ); 127 | } 128 | 129 | async enableDebugAsync(): Promise { 130 | this.runSync("(() => { pxt.options.debug = 1 })()"); 131 | } 132 | 133 | async setCompileSwitchesAsync(flags: string): Promise { 134 | this.runSync(`(() => { 135 | pxt.setCompileSwitches(${JSON.stringify(flags)}); 136 | if (pxt.appTarget.compile.switches.asmdebug) 137 | ts.pxtc.assembler.debug = 1 138 | })()`) 139 | } 140 | 141 | async buildSimJsInfoAsync(result: CompileResult): Promise { 142 | return this.runFunctionSync("pxtc.buildSimJsInfo", [result]) 143 | } 144 | 145 | private runScript(content: string, filename: string) { 146 | const scr = new vm.Script(content, { 147 | filename: filename, 148 | }) 149 | scr.runInContext(this.sandbox) 150 | } 151 | 152 | private runWithCb(code: string, cb: (err: any, res: any) => void) { 153 | this.sandbox._gcb = cb 154 | const src = "(() => { const _cb = _gcb; _gcb = null; " + code + " })()" 155 | const scr = new vm.Script(src) 156 | scr.runInContext(this.sandbox) 157 | } 158 | 159 | private runAsync(code: string) { 160 | const src = 161 | `Promise.resolve().then(() => ${code})` + 162 | `.then(v => _cb(null, v), err => _cb(err.stack || "" + err, null))` 163 | return new Promise((resolve, reject) => 164 | this.runWithCb(src, (err, res) => 165 | err ? reject(new Error(err)) : resolve(res) 166 | ) 167 | ) 168 | } 169 | 170 | private runSync(code: string): any { 171 | const src = 172 | `try { _cb(null, ${code}) } ` + 173 | `catch (err) { _cb(err.stack || "" + err, null) }` 174 | let errRes = null 175 | let normRes = null 176 | this.runWithCb(src, (err, res) => 177 | err ? (errRes = err) : (normRes = res) 178 | ) 179 | if (errRes) throw new Error(errRes) 180 | return normRes 181 | } 182 | 183 | private runFunctionSync(name: string, args: any[]) { 184 | return this.runSync(this.runFunctionCore(name, args)) 185 | } 186 | 187 | private runFunctionAsync(name: string, args: any[]) { 188 | return this.runAsync(this.runFunctionCore(name, args)) 189 | } 190 | 191 | private runFunctionCore(name: string, args: any[]) { 192 | let argString = "" 193 | for (let i = 0; i < args.length; ++i) { 194 | const arg = "_arg" + i 195 | this.sandbox[arg] = args[i] 196 | if (argString) argString += ", " 197 | argString += arg 198 | } 199 | return `${name}(${argString})` 200 | } 201 | } -------------------------------------------------------------------------------- /packages/makecode-browser/src/languageService.ts: -------------------------------------------------------------------------------- 1 | import { WebConfig } from "makecode-core/built/downloader"; 2 | import { BuiltSimJsInfo, CompileOptions, CompileResult } from "makecode-core/built/service"; 3 | import { LanguageService, SimpleDriverCallbacks } from "makecode-core/built/host"; 4 | import { DownloadedEditor, Package } from "makecode-core"; 5 | import { workerJs } from "./workerFiles"; 6 | 7 | export class BrowserLanguageService implements LanguageService { 8 | protected nextID = 0; 9 | protected pendingMessages: { [index: string]: (response: ClientToWorkerRequestResponse) => void }; 10 | protected worker: Worker; 11 | protected driverCallbacks: SimpleDriverCallbacks; 12 | 13 | constructor(public editor: DownloadedEditor) { 14 | this.pendingMessages = {}; 15 | 16 | let workerSource = `var pxtTargetBundle = ${JSON.stringify(this.editor.targetJson)};\n` 17 | workerSource += this.editor.pxtWorkerJs + "\n"; 18 | workerSource += workerJs; 19 | 20 | const workerBlob = new Blob([workerSource], { type: "application/javascript" }); 21 | 22 | this.worker = new Worker(URL.createObjectURL(workerBlob)); 23 | this.worker.onmessage = (ev) => { 24 | if (ev.data.kind) { 25 | this.onWorkerRequestReceived(ev.data); 26 | } 27 | else { 28 | this.onWorkerResponseReceived(ev.data); 29 | } 30 | } 31 | } 32 | 33 | dispose() { 34 | this.worker.terminate(); 35 | } 36 | 37 | async registerDriverCallbacksAsync(callbacks: SimpleDriverCallbacks): Promise { 38 | this.driverCallbacks = callbacks; 39 | 40 | await this.sendWorkerRequestAsync({ 41 | type: "registerDriverCallbacks" 42 | }); 43 | } 44 | 45 | async setWebConfigAsync(config: WebConfig): Promise { 46 | await this.sendWorkerRequestAsync({ 47 | type: "setWebConfig", 48 | webConfig: config as any 49 | }); 50 | } 51 | 52 | async getWebConfigAsync(): Promise { 53 | const res = await this.sendWorkerRequestAsync({ 54 | type: "getWebConfig" 55 | }) as GetWebConfigResponse; 56 | 57 | return res.webConfig as any; 58 | } 59 | 60 | async getAppTargetAsync(): Promise { 61 | const res = await this.sendWorkerRequestAsync({ 62 | type: "getAppTarget" 63 | }) as GetAppTargetResponse; 64 | 65 | return res.appTarget; 66 | } 67 | 68 | async getTargetConfigAsync(): Promise { 69 | return this.editor.targetConfig; 70 | } 71 | 72 | async supportsGhPackagesAsync(): Promise { 73 | const res = await this.sendWorkerRequestAsync({ 74 | type: "supportsGhPackages" 75 | }) as SupportsGhPackagesResponse; 76 | 77 | return res.supported; 78 | } 79 | 80 | async setHwVariantAsync(variant: string): Promise { 81 | await this.sendWorkerRequestAsync({ 82 | type: "setHwVariant", 83 | variant 84 | }); 85 | } 86 | 87 | async getHardwareVariantsAsync(): Promise { 88 | const res = await this.sendWorkerRequestAsync({ 89 | type: "getHardwareVariants" 90 | }) as GetHardwareVariantsResponse; 91 | 92 | return res.configs; 93 | } 94 | 95 | async getBundledPackageConfigsAsync(): Promise { 96 | const res = await this.sendWorkerRequestAsync({ 97 | type: "getBundledPackageConfigs" 98 | }) as GetBundledPackageConfigsResponse; 99 | 100 | return res.configs; 101 | } 102 | 103 | async getCompileOptionsAsync(prj: Package, simpleOpts?: any): Promise { 104 | const res = await this.sendWorkerRequestAsync({ 105 | type: "getCompileOptionsAsync", 106 | opts: simpleOpts 107 | }) as GetCompileOptionsAsyncResponse; 108 | 109 | return res.result; 110 | } 111 | 112 | async installGhPackagesAsync(projectFiles: pxt.Map): Promise> { 113 | const res = await this.sendWorkerRequestAsync({ 114 | type: "installGhPackagesAsync", 115 | files: projectFiles 116 | }) as InstallGhPackagesAsyncResponse; 117 | 118 | return res.result; 119 | } 120 | 121 | async setProjectTextAsync(projectFiles: pxt.Map): Promise { 122 | await this.sendWorkerRequestAsync({ 123 | type: "setProjectText", 124 | files: projectFiles 125 | }); 126 | } 127 | 128 | async performOperationAsync(op: string, options: any): Promise { 129 | const res = await this.sendWorkerRequestAsync({ 130 | type: "performOperation", 131 | op: op, 132 | data: options 133 | }) as PerformOperationResponse; 134 | 135 | return res.result; 136 | } 137 | 138 | async enableExperimentalHardwareAsync(): Promise { 139 | await this.sendWorkerRequestAsync({ 140 | type: "enableExperimentalHardware", 141 | }); 142 | } 143 | 144 | async enableDebugAsync(): Promise { 145 | await this.sendWorkerRequestAsync({ 146 | type: "enableDebug", 147 | }); 148 | } 149 | 150 | async setCompileSwitchesAsync(flags: string): Promise { 151 | await this.sendWorkerRequestAsync({ 152 | type: "setCompileSwitches", 153 | flags 154 | }); 155 | } 156 | 157 | async buildSimJsInfoAsync(result: CompileResult): Promise { 158 | // If you want to implement this, figure out how to get the worker to run the pxtc.buildSimJsInfo function 159 | throw new Error("Not implemented") 160 | } 161 | 162 | protected sendWorkerRequestAsync(message: ClientToWorkerRequest): Promise { 163 | message.id = this.nextID++; 164 | 165 | return new Promise(resolve => { 166 | this.pendingMessages[message.id] = resolve; 167 | this.worker.postMessage(message); 168 | }); 169 | } 170 | 171 | protected onWorkerResponseReceived(message: ClientToWorkerRequestResponse) { 172 | if (this.pendingMessages[message.id]) { 173 | this.pendingMessages[message.id](message); 174 | delete this.pendingMessages[message.id]; 175 | } 176 | else { 177 | console.warn("Received message with no callback"); 178 | } 179 | } 180 | 181 | protected async onWorkerRequestReceived(message: WorkerToClientRequest) { 182 | switch (message.type) { 183 | case "cacheGet": 184 | this.sendWorkerRequestResponse({ 185 | ...message, 186 | response: true, 187 | value: await this.driverCallbacks.cacheGet(message.key) 188 | }); 189 | break; 190 | case "cacheSet": 191 | await this.driverCallbacks.cacheSet(message.key, message.value); 192 | this.sendWorkerRequestResponse({ 193 | ...message, 194 | response: true 195 | }); 196 | break; 197 | case "packageOverride": 198 | this.sendWorkerRequestResponse({ 199 | ...message, 200 | response: true, 201 | files: await this.driverCallbacks.pkgOverrideAsync(message.packageId) 202 | }); 203 | break; 204 | } 205 | } 206 | 207 | protected sendWorkerRequestResponse(message: WorkerToClientRequestResponse) { 208 | this.worker.postMessage(message); 209 | } 210 | } -------------------------------------------------------------------------------- /packages/makecode-browser/worker/worker.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | let _scriptText: pxt.Map; 4 | let nextId = 0; 5 | let pendingMessages: pxt.Map<(response: WorkerToClientRequestResponse) => void> = {}; 6 | 7 | function registerDriverCallbacks() { 8 | // Proxy these to the client 9 | pxt.setupSimpleCompile({ 10 | cacheGet: async key => { 11 | const res = await sendRequestAsync({ 12 | kind: "worker-to-client", 13 | type: "cacheGet", 14 | key 15 | }) as CacheGetResponse; 16 | 17 | return res.value; 18 | }, 19 | cacheSet: async (key, value) => { 20 | await sendRequestAsync({ 21 | kind: "worker-to-client", 22 | type: "cacheSet", 23 | key, 24 | value 25 | }) as CacheSetResponse; 26 | }, 27 | pkgOverrideAsync: async id => { 28 | const res = await sendRequestAsync({ 29 | kind: "worker-to-client", 30 | type: "packageOverride", 31 | packageId: id 32 | }) as PackageOverrideResponse; 33 | 34 | return res.files; 35 | } 36 | }); 37 | } 38 | 39 | function setWebConfig(config: any) { 40 | pxt.setupWebConfig(config); 41 | } 42 | 43 | function getWebConfig() { 44 | return pxt.webConfig; 45 | } 46 | 47 | function getAppTarget() { 48 | return pxt.appTarget; 49 | } 50 | 51 | function supportsGhPackages() { 52 | return !!pxt.simpleInstallPackagesAsync; 53 | } 54 | 55 | function setHwVariant(variant: string) { 56 | pxt.setHwVariant(variant); 57 | } 58 | 59 | function getHardwareVariants() { 60 | return pxt.getHwVariants(); 61 | } 62 | 63 | function getBundledPackageConfigs() { 64 | return Object.values(pxt.appTarget.bundledpkgs).map(pkg => JSON.parse(pkg['pxt.json'])); 65 | } 66 | 67 | function getCompileOptionsAsync(opts: pxt.SimpleCompileOptions) { 68 | return pxt.simpleGetCompileOptionsAsync(_scriptText, opts) 69 | } 70 | 71 | function installGhPackagesAsync(projectFiles: pxt.Map) { 72 | return pxt.simpleInstallPackagesAsync(projectFiles); 73 | } 74 | 75 | function performOperation(op: string, data: any) { 76 | return pxtc.service.performOperation(op as any, data); 77 | } 78 | 79 | function setProjectText(text: pxt.Map) { 80 | _scriptText = text; 81 | } 82 | 83 | function enableExperimentalHardware() { 84 | pxt.savedAppTheme().experimentalHw = true; 85 | pxt.reloadAppTargetVariant(); 86 | } 87 | 88 | function enableDebug() { 89 | pxt.options.debug = true; 90 | } 91 | 92 | function setCompileSwitches(flags: string) { 93 | pxt.setCompileSwitches(flags); 94 | if ((pxt.appTarget.compile.switches as any).asmdebug) { 95 | ts.pxtc.assembler.debug = true 96 | } 97 | } 98 | 99 | function onMessageReceived(message: WorkerToClientRequestResponse | ClientToWorkerRequest) { 100 | if ((message as WorkerToClientRequestResponse).kind) { 101 | onResponseReceived(message as WorkerToClientRequestResponse); 102 | } 103 | else { 104 | onRequestReceivedAsync(message as ClientToWorkerRequest); 105 | } 106 | } 107 | 108 | async function onRequestReceivedAsync(request: ClientToWorkerRequest) { 109 | switch (request.type) { 110 | case "registerDriverCallbacks": 111 | registerDriverCallbacks(); 112 | sendResponse({ 113 | ...request, 114 | response: true 115 | }); 116 | break; 117 | case "setWebConfig": 118 | setWebConfig(request.webConfig); 119 | sendResponse({ 120 | ...request, 121 | response: true 122 | }); 123 | break; 124 | case "getWebConfig": 125 | sendResponse({ 126 | ...request, 127 | webConfig: getWebConfig(), 128 | response: true 129 | }); 130 | break; 131 | case "getAppTarget": 132 | sendResponse({ 133 | ...request, 134 | appTarget: getAppTarget(), 135 | response: true 136 | }); 137 | break; 138 | case "supportsGhPackages": 139 | sendResponse({ 140 | ...request, 141 | supported: supportsGhPackages(), 142 | response: true 143 | }); 144 | break; 145 | case "setHwVariant": 146 | setHwVariant(request.variant); 147 | sendResponse({ 148 | ...request, 149 | response: true 150 | }) 151 | break; 152 | case "getHardwareVariants": 153 | sendResponse({ 154 | ...request, 155 | configs: getHardwareVariants(), 156 | response: true 157 | }); 158 | break; 159 | case "getBundledPackageConfigs": 160 | sendResponse({ 161 | ...request, 162 | configs: getBundledPackageConfigs(), 163 | response: true 164 | }); 165 | break; 166 | case "getCompileOptionsAsync": 167 | sendResponse({ 168 | ...request, 169 | result: await getCompileOptionsAsync(request.opts), 170 | response: true 171 | }); 172 | break; 173 | case "installGhPackagesAsync": 174 | await installGhPackagesAsync(request.files) 175 | sendResponse({ 176 | ...request, 177 | result: request.files, 178 | response: true 179 | }); 180 | break; 181 | case "performOperation": 182 | sendResponse({ 183 | ...request, 184 | result: performOperation(request.op, request.data), 185 | response: true 186 | }); 187 | break; 188 | case "setProjectText": 189 | setProjectText(request.files); 190 | sendResponse({ 191 | ...request, 192 | response: true 193 | }); 194 | break; 195 | case "enableExperimentalHardware": 196 | enableExperimentalHardware(); 197 | sendResponse({ 198 | ...request, 199 | response: true 200 | }); 201 | break; 202 | case "enableDebug": 203 | enableDebug(); 204 | sendResponse({ 205 | ...request, 206 | response: true 207 | }); 208 | break; 209 | case "setCompileSwitches": 210 | setCompileSwitches(request.flags); 211 | sendResponse({ 212 | ...request, 213 | response: true 214 | }); 215 | break; 216 | } 217 | } 218 | 219 | function sendResponse(response: ClientToWorkerRequestResponse) { 220 | postMessage(response); 221 | } 222 | 223 | function sendRequestAsync(request: WorkerToClientRequest): Promise { 224 | request.id = nextId++; 225 | 226 | return new Promise(resolve => { 227 | pendingMessages[request.id!] = resolve; 228 | postMessage(request); 229 | }); 230 | } 231 | 232 | function onResponseReceived(message: WorkerToClientRequestResponse) { 233 | if (pendingMessages[message.id!]) { 234 | pendingMessages[message.id!](message); 235 | delete pendingMessages[message.id!]; 236 | } 237 | else { 238 | console.warn("Worker received message with no callback"); 239 | } 240 | } 241 | 242 | addEventListener("message", ev => { 243 | onMessageReceived(ev.data); 244 | }); -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MKC - command line tool for MakeCode editors 2 | 3 | This package includes a tool that can compile MakeCode (PXT) projects by 4 | downloading parts of a released MakeCode web app and running them in node.js. 5 | 6 | This is different than `pxt` command line tool, which is used primarily during 7 | development of MakeCode editors. 8 | 9 | ## Installation 10 | 11 | Make sure to install [node.js](https://nodejs.org/). 12 | 13 | To install `mkc` globally, run 14 | 15 | ```bash 16 | npm install -g makecode 17 | ``` 18 | 19 | **Do not install the npm `mkc` package, it is another package.** 20 | 21 | To update mkc, 22 | 23 | ```bash 24 | npm install -u -g makecode 25 | ``` 26 | 27 | ## Usage 28 | 29 | The command line tool can be invoked as **`makecode`** or **`mkc`** for short. 30 | 31 | ### mkc init 32 | 33 | To start a new [micro:bit](https://makecode.microbit.org) project in an empty folder: 34 | 35 | ```bash 36 | mkc init microbit 37 | ``` 38 | 39 | where `microbit` is the template name. To get the list of supported templates, do `mkc help init`. 40 | 41 | It is possible to specify a list of dependencies to be added to the template. 42 | 43 | ```bash 44 | mkc init microbit jacdac jacdac-button jacdac-led 45 | ``` 46 | 47 | Your project is ready to be edited. If you are a Visual Studio Code user, type `code .` and you're ready to go! 48 | 49 | ### mkc install 50 | 51 | This command downloads the sources of extensions to the file system so that your TypeScript 52 | IDE can use them 53 | 54 | ```bash 55 | mkc install 56 | ``` 57 | 58 | ### mkc build 59 | 60 | In a folder with `pxt.json` file, run the build command. 61 | 62 | ```bash 63 | mkc build 64 | ``` 65 | 66 | Build is also the default command, so you can just leave it out. 67 | 68 | ```bash 69 | mkc 70 | ``` 71 | 72 | You can also pass `--hw f4`, `--hw d5` etc. Try `--hw help` to get a list. 73 | Use `mkc -j` to build JavaScript (it defaults to native). 74 | 75 | To build and deploy to a device add `-d`. 76 | 77 | ```bash 78 | mkc -d 79 | ``` 80 | 81 | The tool checks once a day if the MakeCode editor has been updated. However, you can force an update by using `--update` 82 | during a build. 83 | 84 | ```bash 85 | mkc --update 86 | ``` 87 | 88 | #### mkc build --watch 89 | 90 | Use `--watch`, or `-w`, with `mkc build` to automatically watch changes in source files and rebuild automatically. 91 | 92 | ```bash 93 | mkc -w 94 | ``` 95 | 96 | #### mkc build compile switches 97 | 98 | Options can be passed to PXT compiler using `--compile-flags` (`-f`) option: 99 | 100 | ```bash 101 | mkc -f size # generate .csv file with function sizes 102 | mkc -f asmdebug # generate more comments in assembly listing 103 | mkc -f profile # enable profiling counters 104 | mkc -f rawELF # don't generate .UF2 but a raw ELF file 105 | mkc -f size,asmdebug # example with two options 106 | ``` 107 | 108 | The same options (except for `asmdebug`) can be passed to website with `?compiler=...` or `?compile=...` argument 109 | and to the regular `pxt` command line utility with `PXT_COMPILE_SWITCHES=...`. 110 | 111 | #### Built files in containers, GitHub Codespace, ... 112 | 113 | To access the build files from a remote machine, 114 | 115 | - open Visual Studio Code 116 | - browse to the `built` folder 117 | - right click `Download` on the desired file. 118 | 119 | ### mkc serve 120 | 121 | Use `mkc serve` to start a watch-build and localhost server with simulator. 122 | Defaults to http://127.0.0.1:7001 123 | 124 | ```bash 125 | mkc serve 126 | ``` 127 | 128 | You can change the port using `port`. 129 | 130 | ```bash 131 | mkc serve --port 7002 132 | ``` 133 | 134 | By default, the simulator ignores `loader.js`. If you have modifications in that file, use ``--force-local`` to use your `loader.js`. 135 | 136 | ```bash 137 | mkc serve --force-local 138 | ``` 139 | 140 | ### mkc clean 141 | 142 | Run the clean command to erase build artifacts and cached packages. 143 | 144 | ```bash 145 | mkc clean 146 | ``` 147 | 148 | ### mkc search 149 | 150 | Search for extensions hosted on GitHub. 151 | 152 | ```bash 153 | mkc search jacdac 154 | ``` 155 | 156 | You can use the result with the `add` command to add extensions to your project. 157 | 158 | ### mkc add 159 | 160 | Adds a new dependency to the project. Pass a GitHub repository URL to the `add` command. 161 | 162 | ```bash 163 | mkc add https://github.com/microsoft/pxt-jacdac/button 164 | ``` 165 | 166 | For Jacdac extensions, simply write `jacdac-servicename` 167 | 168 | ```bash 169 | mkc add jacdac-button 170 | ``` 171 | 172 | ### mkc bump 173 | 174 | Interactive update of the version number of the current project 175 | and all nested projects in a mono-repo. 176 | 177 | ```bash 178 | mkc bump 179 | ``` 180 | 181 | Use `--major`, `--minor`, `--patch` to automatically increment the version number. 182 | 183 | ```bash 184 | mkc bump --patch 185 | ``` 186 | 187 | Adding `--version-file` will make `mkc` write a TypeScript file with the version number. 188 | 189 | ```bash 190 | mkc bump --version-file version.ts 191 | ``` 192 | 193 | Add `--stage` to test the bump without pushing to git. 194 | 195 | ```bash 196 | mkc bump --stage 197 | ``` 198 | 199 | ### mkc download 200 | 201 | Downloads a shared MakeCode project to files and initializes the project. 202 | 203 | ```bash 204 | mkc download https://..... 205 | ``` 206 | 207 | ## Advanced Configuration 208 | 209 | The `init` commands creates a `mkc.json` file that you can also use for additional configurations. 210 | 211 | ```json 212 | { 213 | "targetWebsite": "https://arcade.makecode.com/beta", 214 | "hwVariant": "samd51", 215 | "links": { 216 | "jacdac": "../../pxt-jacdac" 217 | }, 218 | "overrides": { 219 | "testDependencies": {} 220 | }, 221 | "include": ["../../common-mkc.json"] 222 | } 223 | ``` 224 | 225 | All fields are optional. 226 | 227 | - **targetWebsite** says where to take the compiler from; if you omit it, it will be guessed based on packages used by `pxt.json`; 228 | you can point this to a live or beta version of the editor, as well as to a specific version (including SHA-indexed uploads 229 | generated during PXT target builds) 230 | - **hwVariant** specifies default hardware variant (currently only used in Arcade); try `--hw help` command line option to list variants 231 | - **links** overrides specific packages; these can be github packages or built-in packages 232 | - **overrides** is used to override specific keys in `pxt.json` 233 | - files listed in **include** are merged with the keys from the later ones overriding the keys from the earlier ones; 234 | the keys from the current file override all included keys 235 | 236 | You can use `--config-path` or `-c` to build for a different configuration. 237 | 238 | ```bash 239 | mkc -c mkc-arcade.json 240 | ``` 241 | 242 | ## Local development 243 | 244 | This section describes how to build mkc itself. 245 | 246 | mkc is split into three packages: 247 | 248 | 1. makecode-core - which contains most of the functionality/shared code 249 | 2. makecode-node - which contains the node CLI (this is the package that is installed via `npm install makecode`) 250 | 3. makecode-browser - which contains a browser implementation of the mkc language service 251 | 252 | ### Building 253 | 254 | - install node.js 255 | - run `npm install` from the root of this repo (this will also link the local packages) 256 | - start the build watch in makecode-core and makecode-node: 257 | - (run these in separate terminals) 258 | - `cd packages/makecode-core && npm run watch` 259 | - `cd packages/makecode-node && npm run watch` 260 | - run `node path/to/pxt-mkc/packages/makecode-node/makecode` in your project folder 261 | 262 | If you want to test out changes in pxt, first run the build as usual, and then replace 263 | `$HOME/.pxt/mkc-cache/https_58__47__47_-pxtworker.js` 264 | with `pxt/built/web/pxtworker.js`. 265 | Make sure to run `makecode` tool without the `-u` option. 266 | 267 | ### Releases 268 | 269 | To release a package, run the following script to create+push a tagged release: 270 | 271 | ``` 272 | node ./scripts/release.js bump makecode-core 273 | ``` 274 | 275 | After bumping core, to update the CLI package 276 | - update the core version in the package.json of makecode-node 277 | - then `node ./scripts/release.js bump makecode-node` 278 | 279 | ### Contributing 280 | 281 | This project welcomes contributions and suggestions. Most contributions require you to agree to a 282 | Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us 283 | the rights to use your contribution. For details, visit https://cla.opensource.microsoft.com. 284 | 285 | When you submit a pull request, a CLA bot will automatically determine whether you need to provide 286 | a CLA and decorate the PR appropriately (e.g., status check, comment). Simply follow the instructions 287 | provided by the bot. You will only need to do this once across all repos using our CLA. 288 | 289 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 290 | For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or 291 | contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. 292 | -------------------------------------------------------------------------------- /packages/makecode-core/external/pxtpackage.d.ts: -------------------------------------------------------------------------------- 1 | declare namespace pxt { 2 | 3 | type CodeCardType = "file" | "example" | "codeExample" | "tutorial" | "side" | "template" | "package" | "hw" | "forumUrl" | "forumExample" | "sharedExample" | "link"; 4 | type CodeCardEditorType = "blocks" | "js" | "py"; 5 | 6 | interface Map { 7 | [index: string]: T; 8 | } 9 | 10 | interface TargetVersions { 11 | target: string; 12 | targetId?: string; 13 | targetWebsite?: string; 14 | pxt?: string; 15 | pxtCrowdinBranch?: string; 16 | targetCrowdinBranch?: string; 17 | tag?: string; 18 | branch?: string; 19 | commits?: string; // URL 20 | } 21 | 22 | interface Size { 23 | width: number; 24 | height: number; 25 | } 26 | 27 | interface CodeCardAction { 28 | url: string, 29 | editor?: CodeCardEditorType; 30 | cardType?: CodeCardType; 31 | } 32 | 33 | /** 34 | * The schema for the pxt.json package files 35 | */ 36 | interface PackageConfig { 37 | name: string; 38 | version?: string; 39 | // installedVersion?: string; moved to Package class 40 | // url to icon -- support for built-in packages only 41 | icon?: string; 42 | // semver description for support target version 43 | documentation?: string; // doc page to open when loading project, used by sidedocs 44 | targetVersions?: TargetVersions; // versions of the target/pxt the package was compiled against 45 | description?: string; 46 | dependencies: Map; 47 | license?: string; 48 | authors?: string[]; 49 | files: string[]; 50 | simFiles?: string[]; 51 | testFiles?: string[]; 52 | fileDependencies?: Map; // exclude certain files if dependencies are not fulfilled 53 | preferredEditor?: string; // tsprj, blocksprj, pyprj 54 | // languageRestriction?: pxt.editor.LanguageRestriction; // language restrictions that have been placed on the package 55 | testDependencies?: pxt.Map; 56 | cppDependencies?: pxt.Map; 57 | public?: boolean; 58 | partial?: boolean; // true if project is not compileable on its own (eg base) 59 | binaryonly?: boolean; 60 | platformio?: PlatformIOConfig; 61 | compileServiceVariant?: string; 62 | palette?: string[]; 63 | paletteNames?: string[]; 64 | screenSize?: Size; 65 | yotta?: YottaConfig; 66 | codal?: CodalConfig; 67 | npmDependencies?: Map; 68 | card?: CodeCard; 69 | additionalFilePath?: string; 70 | additionalFilePaths?: string[]; 71 | core?: boolean; 72 | // used for sorting for core packages 73 | weight?: number; 74 | gistId?: string; 75 | extension?: PackageExtension; // describe the associated extension if any 76 | dalDTS?: { 77 | corePackage?: string; 78 | includeDirs?: string[]; 79 | excludePrefix?: string[]; 80 | }; 81 | features?: string[]; 82 | hidden?: boolean; // hide package from package selection dialog 83 | skipLocalization?: boolean; 84 | snippetBuilders?: SnippetConfig[]; 85 | experimentalHw?: boolean; 86 | requiredCategories?: string[]; // ensure that those block categories are visible 87 | supportedTargets?: string[]; // a hint about targets in which this extension is supported 88 | firmwareUrl?: string; // link to documentation page about upgrading firmware 89 | disablesVariants?: string[]; // don't build these variants, when this extension is enabled 90 | utf8?: boolean; // force compilation with UTF8 enabled 91 | disableTargetTemplateFiles?: boolean; // do not override target template files when commiting to github 92 | } 93 | 94 | interface PackageExtension { 95 | namespace?: string; // Namespace to add the button under, defaults to package name 96 | label?: string; // Label for the flyout button, defaults to `Editor` 97 | color?: string; // for new category, category color 98 | advanced?: boolean; // for new category, is category advanced 99 | localUrl?: string; // local debugging URL used when served through pxt serve and debugExtensions=1 mode 100 | } 101 | 102 | interface PlatformIOConfig { 103 | dependencies?: Map; 104 | } 105 | 106 | interface CompilationConfig { 107 | description: string; 108 | config: any; 109 | } 110 | 111 | interface CodalConfig { 112 | libraries?: string[]; 113 | } 114 | 115 | interface YottaConfig { 116 | dependencies?: Map; 117 | config?: any; 118 | /** 119 | * Overridable config flags 120 | */ 121 | optionalConfig?: any; 122 | userConfigs?: CompilationConfig[]; 123 | /* deprecated */ 124 | configIsJustDefaults?: boolean; 125 | /* deprecated */ 126 | ignoreConflicts?: boolean; 127 | } 128 | 129 | interface CodeCard { 130 | name?: string; 131 | shortName?: string; 132 | title?: string; 133 | role?: string; 134 | ariaLabel?: string; 135 | label?: string; 136 | labelIcon?: string; 137 | labelClass?: string; 138 | tags?: string[]; // tags shown in home screen, colors specified in theme 139 | tabIndex?: number; 140 | style?: string; // "card" | "item" | undefined; 141 | 142 | color?: string; // one of semantic ui colors 143 | description?: string; 144 | extracontent?: string; 145 | blocksXml?: string; 146 | typeScript?: string; 147 | imageUrl?: string; 148 | largeImageUrl?: string; 149 | videoUrl?: string; 150 | youTubeId?: string; 151 | youTubePlaylistId?: string; // playlist this video belongs to 152 | buttonLabel?: string; 153 | time?: number; 154 | url?: string; 155 | learnMoreUrl?: string; 156 | buyUrl?: string; 157 | feedbackUrl?: string; 158 | responsive?: boolean; 159 | cardType?: CodeCardType; 160 | editor?: CodeCardEditorType; 161 | otherActions?: CodeCardAction[]; 162 | directOpen?: boolean; // skip the details view, directly do the card action 163 | 164 | header?: string; 165 | 166 | tutorialStep?: number; 167 | tutorialLength?: number; 168 | 169 | icon?: string; 170 | iconContent?: string; // Text instead of icon name 171 | iconColor?: string; 172 | 173 | onClick?: (e: any) => void; // React event 174 | onLabelClicked?: (e: any) => void; 175 | 176 | target?: string; 177 | className?: string; 178 | variant?: string; 179 | } 180 | 181 | interface JRes { 182 | id: string; // something like "sounds.bark" 183 | data: string; 184 | dataEncoding?: string; // must be "base64" or missing (meaning the same) 185 | icon?: string; // URL (usually data-URI) for the icon 186 | namespace?: string; // used to construct id 187 | mimeType: string; 188 | displayName?: string; 189 | tilemapTile?: boolean; 190 | tileset?: string[]; 191 | } 192 | 193 | type SnippetOutputType = 'blocks' 194 | type SnippetOutputBehavior = /*assumed default*/'merge' | 'replace' 195 | interface SnippetConfig { 196 | name: string; 197 | namespace: string; 198 | group?: string; 199 | label: string; 200 | outputType: SnippetOutputType; 201 | outputBehavior?: SnippetOutputBehavior; 202 | initialOutput?: string; 203 | questions: SnippetQuestions[]; 204 | } 205 | 206 | type SnippetAnswerTypes = 'number' | 'text' | 'variableName' | 'dropdown' | 'spriteEditor' | 'yesno' | string; // TODO(jb) Should include custom answer types for number, enums, string, image 207 | 208 | interface SnippetGoToOptions { 209 | question?: number; 210 | validate?: SnippetValidate; 211 | parameters?: SnippetParameters[]; // Answer token with corresponding question 212 | } 213 | 214 | interface SnippetOutputOptions { 215 | type: 'error' | 'hint'; 216 | output: string; 217 | } 218 | 219 | interface SnippetParameters { 220 | token?: string; 221 | answer?: string; 222 | question: number; 223 | } 224 | 225 | interface SnippetInputAnswerSingular { 226 | answerToken: string; 227 | defaultAnswer: SnippetAnswerTypes; 228 | } 229 | 230 | interface SnippetInputAnswerPlural { 231 | answerTokens: string[]; 232 | defaultAnswers: SnippetAnswerTypes[]; 233 | } 234 | 235 | interface SnippetInputOtherType { 236 | type: string; 237 | } 238 | 239 | interface SnippetInputNumberType { 240 | type: 'number' | 'positionPicker'; 241 | max?: number; 242 | min?: number; 243 | } 244 | 245 | interface SnippetInputDropdownType { 246 | type: "dropdown"; 247 | options: pxt.Map; 248 | } 249 | 250 | interface SnippetInputYesNoType { 251 | type: "yesno"; 252 | } 253 | 254 | type SnippetQuestionInput = { label?: string; } 255 | & (SnippetInputAnswerSingular | SnippetInputAnswerPlural) 256 | & (SnippetInputOtherType | SnippetInputNumberType | SnippetInputDropdownType | SnippetInputYesNoType) 257 | 258 | interface SnippetValidateRegex { 259 | token: string; 260 | regex: string; 261 | match?: SnippetParameters; 262 | noMatch?: SnippetParameters; 263 | } 264 | 265 | interface SnippetValidate { 266 | regex?: SnippetValidateRegex; 267 | } 268 | 269 | interface SnippetQuestions { 270 | title: string; 271 | output?: string; 272 | outputConditionalOnAnswer?: string; 273 | errorMessage?: string; 274 | goto?: SnippetGoToOptions; 275 | inputs: SnippetQuestionInput[]; 276 | hint?: string; 277 | } 278 | } -------------------------------------------------------------------------------- /packages/makecode-node/src/cli.ts: -------------------------------------------------------------------------------- 1 | import { 2 | program as commander, 3 | CommandOptions, 4 | Command, 5 | Argument, 6 | } from "commander"; 7 | 8 | import * as chalk from "chalk"; 9 | import watch from "node-watch" 10 | 11 | import { 12 | ProjectOptions, 13 | BuildOptions, 14 | applyGlobalOptions, 15 | resolveProject, 16 | downloadCommand, 17 | initCommand, 18 | installCommand, 19 | cleanCommand, 20 | addCommand, 21 | searchCommand, 22 | stackCommand, 23 | buildCommandOnce, 24 | shareCommand 25 | } from "makecode-core/built/commands"; 26 | 27 | import { descriptors } from "makecode-core/built/loader"; 28 | import { setHost } from "makecode-core/built/host"; 29 | import { setLogging } from "makecode-core/built/mkc"; 30 | 31 | import { createNodeHost } from "./nodeHost"; 32 | import { bumpAsync } from "./bump"; 33 | import { startSimServer } from "./simserver"; 34 | 35 | let debugMode = false 36 | 37 | function info(msg: string) { 38 | console.log(chalk.blueBright(msg)) 39 | } 40 | 41 | function msg(msg: string) { 42 | console.log(chalk.green(msg)) 43 | } 44 | 45 | function error(msg: string) { 46 | console.error(chalk.red(msg)) 47 | } 48 | 49 | interface BumpOptions extends ProjectOptions { 50 | versionFile?: string 51 | stage?: boolean 52 | patch?: boolean 53 | minor?: boolean 54 | major?: boolean 55 | } 56 | 57 | export async function buildCommand(opts: BuildOptions, info: Command) { 58 | if (info?.args?.length) { 59 | error("invalid command") 60 | process.exit(1) 61 | } 62 | applyGlobalOptions(opts) 63 | if (opts.deploy && opts.monoRepo) { 64 | error("--deploy and --mono-repo cannot be used together") 65 | process.exit(1) 66 | } 67 | if (opts.deploy && opts.javaScript) { 68 | error("--deploy and --java-script cannot be used together") 69 | process.exit(1) 70 | } 71 | if (opts.watch) { 72 | startWatch(opts) 73 | } else await buildCommandOnce(opts) 74 | } 75 | 76 | export async function bumpCommand(opts: BumpOptions) { 77 | applyGlobalOptions(opts) 78 | const prj = await resolveProject(opts) 79 | await bumpAsync(prj, opts?.versionFile, opts?.stage, opts?.major ? "major" : opts?.minor ? "minor" : opts?.patch ? "patch" : undefined) 80 | } 81 | 82 | function clone(v: T): T { 83 | return JSON.parse(JSON.stringify(v)) 84 | } 85 | 86 | function delay(ms: number) { 87 | return new Promise(resolve => setTimeout(resolve, ms)) 88 | } 89 | 90 | interface ServeOptions extends BuildOptions { 91 | port?: string 92 | forceLocal?: boolean 93 | } 94 | async function serveCommand(opts: ServeOptions) { 95 | applyGlobalOptions(opts) 96 | opts.javaScript = true 97 | if (opts.watch) startWatch(clone(opts)) 98 | opts = clone(opts) 99 | opts.update = false 100 | const prj = await resolveProject(opts, !!opts.watch) 101 | const port = parseInt(opts.port) || 7001 102 | const url = `http://127.0.0.1:${port}` 103 | const forceLocal = !!opts.forceLocal 104 | msg(`simulator at ${url}`) 105 | msg(`Jacdac+simulator at https://microsoft.github.io/jacdac-docs/clients/javascript/devtools#${url}`) 106 | startSimServer(prj.editor, port, forceLocal) 107 | } 108 | 109 | function startWatch(opts: BuildOptions) { 110 | const binaries: Record = {} 111 | const watcher = watch("./", { 112 | recursive: true, 113 | delay: 200, 114 | filter(f, skip) { 115 | // skip node_modules, pxt_modules, built, .git 116 | if (/\/?((node|pxt)_modules|built|\.git)/i.test(f)) return skip 117 | // only watch for js files 118 | return /\.(json|ts|asm|cpp|c|h|hpp)$/i.test(f) 119 | }, 120 | }) 121 | 122 | let building = false 123 | let buildPending = false 124 | const build = async (ev: string, filename: string) => { 125 | if (ev) msg(`detected ${ev} ${filename}`) 126 | 127 | buildPending = true 128 | 129 | await delay(100) // wait for other change events, that might have piled-up to arrive 130 | 131 | // don't trigger 2 build, wait and do it again 132 | if (building) { 133 | msg(` build in progress, waiting...`) 134 | return 135 | } 136 | 137 | // start a build 138 | try { 139 | building = true 140 | while (buildPending) { 141 | buildPending = false 142 | const opts0 = clone(opts) 143 | if (ev) 144 | // if not first time, don't update 145 | opts0.update = false 146 | const files = (await buildCommandOnce(opts0)).outfiles 147 | if (files) 148 | Object.entries(files).forEach(([key, value]) => { 149 | if (/\.(hex|json|asm)$/.test(key)) binaries[key] = value 150 | else binaries[key] = Buffer.from(value, "base64") 151 | }) 152 | } 153 | } catch (e) { 154 | error(e) 155 | } finally { 156 | building = false 157 | } 158 | } 159 | watcher.on("change", build) 160 | msg(`start watching for file changes`) 161 | build(undefined, undefined) 162 | } 163 | 164 | function createCommand(name: string, opts?: CommandOptions) { 165 | const cmd = commander 166 | .command(name, opts) 167 | .option("--colors", "force color output") 168 | .option("--no-colors", "disable color output") 169 | .option("--debug", "enable debug output from PXT") 170 | .option("-f, --compile-flags ", 171 | "set PXT compiler options (?compile=... or PXT_COMPILE_SWITCHES=... in other tools)") 172 | return cmd 173 | } 174 | 175 | async function mainCli() { 176 | setHost(createNodeHost()); 177 | 178 | setLogging({ 179 | log: info, 180 | error: error, 181 | debug: s => { 182 | if (debugMode) 183 | console.debug(chalk.gray(s)) 184 | }, 185 | }) 186 | 187 | commander.version(require("../package.json").version) 188 | 189 | createCommand("build", { isDefault: true }) 190 | .description("build project") 191 | .option("-w, --watch", "watch source files and rebuild on changes") 192 | .option("-n, --native", "compile native (default)") 193 | .option("-d, --deploy", "copy resulting binary to UF2 or HEX drive") 194 | .option( 195 | "-h, --hw ", 196 | "set hardware(s) for which to compile (implies -n)" 197 | ) 198 | .option("-j, --java-script", "compile to JavaScript") 199 | .option("-u, --update", "check for web-app updates") 200 | .option( 201 | "-c, --config-path ", 202 | 'set configuration file path (default: "mkc.json")' 203 | ) 204 | .option( 205 | "-r, --mono-repo", 206 | "also build all subfolders with 'pxt.json' in them" 207 | ) 208 | .option( 209 | "--always-built", 210 | "always generate files in built/ folder (and not built/hw-variant/)" 211 | ) 212 | .action(buildCommand) 213 | 214 | createCommand("serve") 215 | .description("start local simulator web server") 216 | .option("--no-watch", "do not watch source files") 217 | .option("-p, --port ", "port to listen at, default to 7001") 218 | .option("-u, --update", "check for web-app updates") 219 | .option( 220 | "-c, --config-path ", 221 | 'set configuration file path (default: "mkc.json")' 222 | ) 223 | .option("--force-local", "force using all local files") 224 | .action(serveCommand) 225 | 226 | createCommand("download") 227 | .argument( 228 | "", 229 | "url to the shared project from your makecode editor" 230 | ) 231 | .description("download project from share URL") 232 | .action(downloadCommand) 233 | 234 | createCommand("bump") 235 | .description( 236 | "interactive version incrementer for a project or mono-repo" 237 | ) 238 | .option( 239 | "--version-file ", 240 | "write generated version number into the file" 241 | ) 242 | .option("--stage", "skip git commit and push operations") 243 | .option("--patch", "auto-increment patch version number") 244 | .option("--minor", "auto-increment minor version number") 245 | .option("--major", "auto-increment major version number") 246 | .action(bumpCommand) 247 | 248 | createCommand("init") 249 | .addArgument( 250 | new Argument("[template]", "project template name").choices( 251 | descriptors.map(d => d.id) 252 | ) 253 | ) 254 | .argument("[repo...]", "dependencies to be added to the project") 255 | .description( 256 | "initializes the project, downloads the dependencies, optionally for a particular editor" 257 | ) 258 | .option( 259 | "--symlink-pxt-modules", 260 | "symlink files in pxt_modules/* for auto-completion" 261 | ) 262 | .option( 263 | "--link-pxt-modules", 264 | "write pxt_modules/* adhering to 'links' field in mkc.json (for pxt cli build)" 265 | ) 266 | .action(initCommand) 267 | 268 | createCommand("install") 269 | .description("downloads the dependencies") 270 | .option( 271 | "-r, --mono-repo", 272 | "also install in all subfolders with 'pxt.json' in them" 273 | ) 274 | .option( 275 | "--symlink-pxt-modules", 276 | "symlink files in pxt_modules/* for auto-completion" 277 | ) 278 | .option( 279 | "--link-pxt-modules", 280 | "write pxt_modules/* adhering to 'links' field in mkc.json (for pxt cli build)" 281 | ) 282 | .action(installCommand) 283 | 284 | createCommand("clean") 285 | .description("deletes built artifacts") 286 | .action(cleanCommand) 287 | 288 | createCommand("add") 289 | .argument("", "url to the github repository") 290 | .argument("[name]", "name of the dependency") 291 | .description("add new dependencies") 292 | .option( 293 | "-c, --config-path ", 294 | 'set configuration file path (default: "mkc.json")' 295 | ) 296 | .action(addCommand) 297 | 298 | createCommand("search") 299 | .argument("", "extension to search for") 300 | .description("search for an extension") 301 | .option( 302 | "-c, --config-path ", 303 | 'set configuration file path (default: "mkc.json")' 304 | ) 305 | .action(searchCommand) 306 | 307 | createCommand("share") 308 | .description("creates a public share link for the project") 309 | .action(shareCommand) 310 | 311 | createCommand("stack", { hidden: true }) 312 | .description("expand stack trace") 313 | .action(stackCommand) 314 | 315 | await commander.parseAsync(process.argv) 316 | } 317 | 318 | async function mainWrapper() { 319 | try { 320 | await mainCli() 321 | } catch (e) { 322 | error("Exception: " + e.stack) 323 | error("Build failed") 324 | process.exit(1) 325 | } 326 | } 327 | 328 | mainWrapper() 329 | -------------------------------------------------------------------------------- /scripts/release.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const child_process = require("child_process"); 3 | const fs = require("fs"); 4 | const os = require("os"); 5 | const https = require("https"); 6 | const http = require("http"); 7 | const url = require("url"); 8 | const zlib = require("zlib"); 9 | 10 | const packages = { 11 | "makecode-core": { 12 | name: "makecode-core", 13 | aliases: ["core", "c"] 14 | }, 15 | "makecode-node": { 16 | name: "makecode", 17 | aliases: ["node", "n"] 18 | }, 19 | "makecode-browser": { 20 | name: "makecode-browser", 21 | aliases: ["browser", "b"] 22 | } 23 | }; 24 | 25 | const args = process.argv.slice(2); 26 | const root = path.resolve(__dirname, ".."); 27 | 28 | const commandArg = args[0].toLowerCase(); 29 | 30 | if (commandArg === "bump") { 31 | bump(getPackageDirectory(args[1].toLowerCase()), (args[2] || "patch").toLowerCase()); 32 | } 33 | else if (commandArg === "publish") { 34 | publish(); 35 | } 36 | else { 37 | console.error("Invalid command"); 38 | printUsage(); 39 | process.exit(1); 40 | } 41 | 42 | async function bump(packageDirectory, versionType) { 43 | if (!isWorkingDirectoryClean()) { 44 | console.error("Working git directory not clean. Aborting"); 45 | process.exit(1); 46 | } 47 | 48 | const versionTypes = ["patch", "minor", "major"]; 49 | if (versionType && versionTypes.indexOf(versionType) === -1) { 50 | console.error("Invalid version type"); 51 | printUsage(); 52 | process.exit(1); 53 | } 54 | 55 | const token = await getGitHubTokenAsync(); 56 | 57 | exec("git fetch origin master", root); 58 | exec("git checkout master", root); 59 | exec("git merge origin/master --ff-only", root); 60 | const branchName = `release/${timestamp()}`; 61 | exec(`git checkout -b ${branchName}`, root); 62 | exec("npm version " + versionType + " --git-tag-version false", packageDirectory); 63 | 64 | const jsonPath = path.join(packageDirectory, "package.json"); 65 | const json = JSON.parse(fs.readFileSync(jsonPath, "utf-8")); 66 | const version = json.version; 67 | const packageName = json.name; 68 | 69 | const tagName = `${packageName}-v${version}`; 70 | 71 | await spawnWithPipeAsync({ 72 | cmd: "git", 73 | args: ["commit", "-am", `[release] bump version to ${tagName}`], 74 | }); 75 | 76 | exec(`git push origin ${branchName}`, root); 77 | 78 | const url = await createPullRequestAsync({ 79 | title: `[release] bump version to ${tagName}`, 80 | body: "__Do not edit the PR title.__\n" + 81 | "It was automatically generated by `node ./scripts/release.js bump` and must follow a specific pattern.\n" + 82 | "GitHub workflows rely on it to trigger version tagging and publishing to npm.", 83 | head: branchName, 84 | base: "master", 85 | token, 86 | owner: "microsoft", 87 | repo: "pxt-mkc", 88 | }); 89 | 90 | exec(`git checkout master`); 91 | console.log(`Pull request created: ${url}`); 92 | } 93 | 94 | function publish() { 95 | if (process.env.CI !== "true") { 96 | console.error("This command is only meant to be run in GitHub Actions"); 97 | process.exit(0); 98 | } 99 | 100 | const commitMessage = process.env.COMMIT_MESSAGE?.trim(); 101 | const match = /\[release\] bump version to (makecode-core|makecode-browser|makecode)-v[0-9]+\.[0-9]+\.[0-9]+/.exec(commitMessage); 102 | 103 | if (!match) { 104 | console.error("Not a release tag. Aborting"); 105 | process.exit(0); 106 | } 107 | const packageName = match[1]; 108 | 109 | console.log(`Publishing package: ${packageName}`); 110 | const packageDirectory = getPackageDirectory(packageName); 111 | 112 | exec("npm publish", packageDirectory); 113 | } 114 | 115 | function printUsage() { 116 | console.log(`usage: node scripts/release.js bump core|node|browser patch|minor|major`); 117 | } 118 | 119 | function exec(command, cwd) { 120 | console.log(`${command}`) 121 | const result = execCore(command, cwd); 122 | 123 | if (result.status) { 124 | process.exit(result.status); 125 | } 126 | } 127 | 128 | function execCore(command, cwd) { 129 | const args = command.split(" "); 130 | const result = child_process.spawnSync(args[0], args.slice(1), { cwd, stdio: "inherit" }); 131 | 132 | return result; 133 | } 134 | 135 | function isWorkingDirectoryClean() { 136 | const result = execCore("git diff-index --quiet HEAD --", root); 137 | if (result.status) { 138 | return false; 139 | } 140 | return true; 141 | } 142 | 143 | function getPackageDirectory(name) { 144 | let packageDirectory; 145 | for (const key of Object.keys(packages)) { 146 | const info = packages[key]; 147 | 148 | if (key === name || info.name === name || info.aliases.indexOf(name) !== -1) { 149 | packageDirectory = key; 150 | break; 151 | } 152 | } 153 | 154 | if (!packageDirectory) { 155 | console.error("Invalid package"); 156 | printUsage(); 157 | process.exit(1); 158 | } 159 | 160 | return path.join(root, "packages", packageDirectory); 161 | } 162 | 163 | function timestamp(date = new Date()) { 164 | const yyyy = date.getUTCFullYear(); 165 | const mm = String(date.getUTCMonth() + 1).padStart(2, "0"); 166 | const dd = String(date.getUTCDate()).padStart(2, "0"); 167 | const hh = String(date.getUTCHours()).padStart(2, "0"); 168 | const min = String(date.getUTCMinutes()).padStart(2, "0"); 169 | const sec = String(date.getUTCSeconds()).padStart(2, "0"); 170 | return `${yyyy}${mm}${dd}-${hh}${min}${sec}`; 171 | } 172 | 173 | function spawnWithPipeAsync(opts) { 174 | // https://nodejs.org/en/blog/vulnerability/april-2024-security-releases-2 175 | if (os.platform() === "win32" && typeof opts.shell === "undefined") opts.shell = true 176 | if (opts.pipe === undefined) opts.pipe = true 177 | let info = opts.cmd + " " + opts.args.join(" ") 178 | if (opts.cwd && opts.cwd != ".") info = "cd " + opts.cwd + "; " + info 179 | //console.log("[run] " + info) // uncomment for debugging, but it can potentially leak secrets so do not check in 180 | return new Promise((resolve, reject) => { 181 | let ch = child_process.spawn(opts.cmd, opts.args, { 182 | cwd: opts.cwd, 183 | env: opts.envOverrides ? extendEnv(process.env, opts.envOverrides) : process.env, 184 | stdio: opts.pipe ? [opts.input == null ? process.stdin : "pipe", "pipe", process.stderr] : "inherit", 185 | shell: opts.shell || false 186 | } ) 187 | let bufs = [] 188 | if (opts.pipe) 189 | ch.stdout.on('data', (buf) => { 190 | bufs.push(buf) 191 | if (!opts.silent) { 192 | process.stdout.write(buf) 193 | } 194 | }) 195 | ch.on('close', (code) => { 196 | if (code != 0 && !opts.allowNonZeroExit) 197 | reject(new Error("Exit code: " + code + " from " + info)) 198 | resolve(Buffer.concat(bufs)) 199 | }); 200 | if (opts.input != null) 201 | ch.stdin.end(opts.input, "utf8") 202 | }) 203 | } 204 | 205 | async function getGitHubTokenAsync() { 206 | const outputBuf = await spawnWithPipeAsync({ 207 | cmd: "git", 208 | args: ["credential", "fill"], 209 | input: "protocol=https\nhost=github.com\n\n", 210 | silent: true 211 | }); 212 | 213 | const output = outputBuf.toString("utf8").trim(); 214 | const lines = output.split("\n"); 215 | const creds = {}; 216 | for (const line of lines) { 217 | const [key, ...rest] = line.split("="); 218 | creds[key] = rest.join("="); 219 | } 220 | 221 | if (creds.password) { 222 | return creds.password; 223 | } else { 224 | throw new Error("No GitHub credentials found via git credential helper."); 225 | } 226 | } 227 | 228 | function httpRequestCoreAsync(options) { 229 | let isHttps = false 230 | 231 | let u = url.parse(options.url) 232 | 233 | if (u.protocol == "https:") isHttps = true 234 | else if (u.protocol == "http:") isHttps = false 235 | else return Promise.reject("bad protocol: " + u.protocol) 236 | 237 | u.headers = options.headers || {} 238 | let data = options.data 239 | u.method = options.method || (data == null ? "GET" : "POST"); 240 | 241 | let buf = null; 242 | 243 | u.headers["accept-encoding"] = "gzip" 244 | u.headers["user-agent"] = "PXT-CLI" 245 | 246 | let gzipContent = false 247 | 248 | if (data != null) { 249 | if (Buffer.isBuffer(data)) { 250 | buf = data; 251 | } else if (typeof data == "object") { 252 | buf = Buffer.from(JSON.stringify(data), "utf8") 253 | u.headers["content-type"] = "application/json; charset=utf8" 254 | if (options.allowGzipPost) gzipContent = true 255 | } else if (typeof data == "string") { 256 | buf = Buffer.from(data, "utf8") 257 | if (options.allowGzipPost) gzipContent = true 258 | } else { 259 | throw new Error("bad data") 260 | } 261 | } 262 | 263 | if (gzipContent) { 264 | buf = zlib.gzipSync(buf) 265 | u.headers['content-encoding'] = "gzip" 266 | } 267 | 268 | if (buf) 269 | u.headers['content-length'] = buf.length 270 | 271 | return new Promise((resolve, reject) => { 272 | const handleResponse = (res) => { 273 | let g = res; 274 | if (/gzip/.test(res.headers['content-encoding'])) { 275 | let tmp = zlib.createUnzip(); 276 | res.pipe(tmp); 277 | g = tmp; 278 | } 279 | 280 | resolve(readResAsync(g).then(buf => { 281 | let text = null 282 | let json = null 283 | try { 284 | text = buf.toString("utf8") 285 | json = JSON.parse(text) 286 | } catch (e) { 287 | } 288 | let resp = { 289 | statusCode: res.statusCode, 290 | headers: res.headers, 291 | buffer: buf, 292 | text: text, 293 | json: json, 294 | } 295 | return resp; 296 | })) 297 | }; 298 | 299 | const req = isHttps ? https.request(u, handleResponse) : http.request(u, handleResponse); 300 | req.on('error', (err) => reject(err)) 301 | req.end(buf) 302 | }) 303 | } 304 | 305 | function readResAsync(g) { 306 | return new Promise((resolve, reject) => { 307 | let bufs = [] 308 | g.on('data', (c) => { 309 | if (typeof c === "string") 310 | bufs.push(Buffer.from(c, "utf8")) 311 | else 312 | bufs.push(c) 313 | }); 314 | 315 | g.on("error", (err) => reject(err)) 316 | 317 | g.on('end', () => resolve(Buffer.concat(bufs))) 318 | }) 319 | } 320 | 321 | 322 | async function createPullRequestAsync(opts) { 323 | const { token, owner, repo, title, head, base, body } = opts; 324 | const res = await httpRequestCoreAsync({ 325 | url: `https://api.github.com/repos/${owner}/${repo}/pulls`, 326 | method: "POST", 327 | headers: { 328 | Authorization: `token ${token}`, 329 | "Accept": "application/vnd.github+json", 330 | "Content-Type": "application/json", 331 | }, 332 | data: { 333 | title, 334 | head, 335 | base, 336 | body, 337 | }, 338 | }); 339 | 340 | if (res.statusCode !== 201) { 341 | throw new Error(`Failed to create pull request: ${res.statusCode} ${res.text}`); 342 | } 343 | 344 | const data = await res.json; 345 | return data.html_url; 346 | } -------------------------------------------------------------------------------- /packages/makecode-core/simloader/loader.ts: -------------------------------------------------------------------------------- 1 | type InitFn = (props: { send: (msg: Uint8Array) => void }) => void 2 | type HandlerFn = { 3 | channel: string, 4 | handler: (data: Uint8Array) => void, 5 | init: InitFn 6 | } 7 | 8 | let channelHandlers: { [name: string]: HandlerFn } = {} 9 | let _vsapi: any 10 | 11 | function addSimMessageHandler( 12 | channel: string, 13 | handler: (data: any) => void, 14 | init: (props: { send: (msg: Uint8Array) => void }) => void 15 | ) { 16 | channelHandlers[channel] = { 17 | channel: channel, 18 | init: init, 19 | handler: handler, 20 | }; 21 | } 22 | 23 | interface FetchResult { 24 | text: string; 25 | srcDoc?: string; 26 | } 27 | 28 | 29 | const pendingMessages: {[index: string]: (result: FetchResult) => void} = {}; 30 | let nextMessageId = 0; 31 | 32 | function makeCodeRun(options) { 33 | let code = ""; 34 | let code0 = ""; 35 | let isReady = false; 36 | let simState = {}; 37 | let simStateChanged = false; 38 | let started = false; 39 | let meta = undefined; 40 | let boardDefinition = undefined; 41 | let simOrigin = undefined; 42 | const selfId = options.selfId || "pxt" + Math.random(); 43 | const tool = options.tool; 44 | const isLocalHost = /^(localhost|127\.0\.0\.1)(:|$)/i.test(window.location.host); 45 | 46 | // hide scrollbar 47 | window.scrollTo(0, 1); 48 | 49 | const lckey = "pxt_frameid_" + tool; 50 | if (!localStorage[lckey]) 51 | localStorage[lckey] = "x" + Math.round(Math.random() * 2147483647); 52 | let frameid = localStorage[lckey]; 53 | 54 | // init runtime 55 | initSimState(); 56 | startCode(); 57 | if (isLocalHost) 58 | autoReload(); 59 | 60 | function fetchSourceCode(): Promise { 61 | if (options.usePostMessage) { 62 | return postMessageToParentAsync({ 63 | type: "fetch-js" 64 | }); 65 | } 66 | return fetch(options.js) 67 | .then(async resp => resp.status == 200 ? { text: await resp.text() } : undefined); 68 | } 69 | 70 | // helpers 71 | function autoReload() { 72 | setInterval(() => { 73 | fetchSourceCode() 74 | .then(c => { 75 | if (c?.text && c.text != code0) 76 | window.location.reload(); 77 | }) 78 | }, 1000) 79 | } 80 | function startCode() { 81 | fetchSourceCode() 82 | .then(c => { 83 | if (!c?.text) return; 84 | const text = c.text; 85 | const srcDoc = c.srcDoc; 86 | code0 = code = text; 87 | // find metadata 88 | code.replace(/^\/\/\s+meta=([^\n]+)\n/m, function (m, metasrc) { 89 | meta = JSON.parse(metasrc); 90 | return ""; 91 | }); 92 | code.replace( 93 | /^\/\/\s+boardDefinition=([^\n]+)\n/m, 94 | function (m, metasrc) { 95 | boardDefinition = JSON.parse(metasrc); 96 | return ""; 97 | } 98 | ); 99 | document.body.dataset.version = meta?.version; 100 | // force local sim 101 | if (isLocalHost) 102 | meta.simUrl = window.location.protocol + "//" + window.location.host + `/sim.html${window.location.search || ""}`; 103 | 104 | const ap = document.getElementById("download-a") as HTMLAnchorElement 105 | if (meta.version && ap && ap.download) 106 | ap.download = ap.download.replace(/VERSION/, meta.version); 107 | 108 | // load simulator with correct version 109 | const iframe = document.getElementById("simframe") as HTMLIFrameElement; 110 | 111 | if (srcDoc) { 112 | iframe.srcdoc = srcDoc; 113 | } 114 | else { 115 | iframe.setAttribute("src", meta.simUrl + "#" + frameid); 116 | let m = /^https?:\/\/[^\/]+/.exec(meta.simUrl); 117 | simOrigin = m[0]; 118 | } 119 | initFullScreen(); 120 | }) 121 | } 122 | 123 | function startSim() { 124 | if (!code || !isReady || started) return; 125 | setState("run"); 126 | const frame = document.getElementById("simframe"); 127 | frame.classList.remove("grayscale"); 128 | started = true; 129 | const runMsg = { 130 | type: "run", 131 | parts: [], 132 | builtinParts: [], 133 | code: code, 134 | partDefinitions: {}, 135 | fnArgs: {}, 136 | cdnUrl: meta.cdnUrl, 137 | version: meta.target, 138 | storedState: simState, 139 | frameCounter: 1, 140 | boardDefinition: boardDefinition, 141 | options: { 142 | theme: "green", 143 | player: "", 144 | }, 145 | id: "green-" + Math.random(), 146 | }; 147 | postMessage(runMsg); 148 | } 149 | 150 | function stopSim() { 151 | setState("stopped"); 152 | const frame = document.getElementById("simframe"); 153 | frame.classList.add("grayscale"); 154 | postMessage({ 155 | type: "stop", 156 | }); 157 | started = false; 158 | } 159 | 160 | window.addEventListener( 161 | "message", 162 | function (ev) { 163 | let d = ev.data; 164 | console.log(ev.origin, d) 165 | 166 | let isSim = false; 167 | 168 | if (simOrigin) { 169 | isSim = ev.origin === simOrigin; 170 | } 171 | else { 172 | const iframe = this.document.getElementById("simframe") as HTMLIFrameElement; 173 | isSim = ev.source === iframe.contentWindow; 174 | } 175 | 176 | if (isSim) { 177 | if (d.req_seq) { 178 | postMessageToParentAsync(d); 179 | return; 180 | } 181 | 182 | if (d.type == "ready") { 183 | let loader = document.getElementById("loader"); 184 | if (loader) loader.remove(); 185 | isReady = true; 186 | startSim(); 187 | } else if (d.type == "simulator") { 188 | switch (d.command) { 189 | case "restart": 190 | if (isLocalHost) { 191 | window.location.reload(); 192 | } else { 193 | stopSim(); 194 | startSim(); 195 | } 196 | break 197 | case "setstate": 198 | if (d.stateValue === null) 199 | delete simState[d.stateKey]; 200 | else simState[d.stateKey] = d.stateValue; 201 | simStateChanged = true; 202 | break 203 | } 204 | } else if (d.type === "debugger") { 205 | // console.log("dbg", d) 206 | let brk = d; 207 | let stackTrace = brk.exceptionMessage + "\n"; 208 | for (let s of brk.stackframes) { 209 | let fi = s.funcInfo; 210 | stackTrace += ` at ${fi.functionName} (${fi.fileName 211 | }:${fi.line + 1}:${fi.column + 1})\n`; 212 | } 213 | if (brk.exceptionMessage) console.error(stackTrace); 214 | postMessageToParentAsync(d); 215 | } else if (d.type === "messagepacket" && d.channel) { 216 | if ( 217 | d.channel == "jacdac" && 218 | d.broadcast && 219 | window.parent != window 220 | ) { 221 | d.sender = selfId; 222 | window.parent.postMessage(d, "*"); 223 | } 224 | const ch = channelHandlers[d.channel] 225 | if (ch) { 226 | try { 227 | ch.handler(d.data); 228 | } catch (e) { 229 | console.log(`invalid simmessage`); 230 | console.log(e); 231 | } 232 | } 233 | } 234 | else if (d.type === "bulkserial") { 235 | postMessageToParentAsync(d); 236 | } 237 | } else { 238 | if ( 239 | d.type == "messagepacket" && 240 | d.channel == "jacdac" && 241 | d.sender != selfId 242 | ) { 243 | postMessage(d); 244 | } else if (d.type == "reload") { 245 | window.location.reload(); 246 | } 247 | else if (d.type == "fetch-js") { 248 | pendingMessages[d.id]({ 249 | text: d.text, 250 | srcDoc: d.srcDoc 251 | }); 252 | delete pendingMessages[d.id]; 253 | } else if (d.type === "stop-sim") { 254 | stopSim(); 255 | } 256 | else if (d.source === "pxtdriver") { 257 | postMessage(d); 258 | } 259 | } 260 | }, 261 | false 262 | ); 263 | 264 | // initialize simmessages 265 | Object.keys(channelHandlers) 266 | .map(k => channelHandlers[k]) 267 | .filter(ch => !!ch.init) 268 | .forEach(ch => { 269 | const send = (msg) => postMessage({ 270 | type: "messagepacket", 271 | channel: ch.channel, 272 | data: msg 273 | }); 274 | ch.init({ send }); 275 | }) 276 | 277 | function setState(st) { 278 | let r = document.getElementById("root"); 279 | if (r) r.setAttribute("data-state", st); 280 | } 281 | 282 | function postMessage(msg) { 283 | const frame = document.getElementById("simframe") as HTMLIFrameElement 284 | if (meta && frame) frame.contentWindow.postMessage(msg, simOrigin ? meta.simUrl : "*"); 285 | } 286 | 287 | function initSimState() { 288 | try { 289 | simState = JSON.parse(localStorage["pxt_simstate"]); 290 | } catch (e) { 291 | simState = {}; 292 | } 293 | setInterval(function () { 294 | if (simStateChanged) { 295 | localStorage["pxt_simstate"] = JSON.stringify(simState); 296 | } 297 | simStateChanged = false; 298 | }, 200) 299 | } 300 | 301 | function initFullScreen() { 302 | var sim = document.getElementById("simframe"); 303 | var fs = document.getElementById("fullscreen"); 304 | if (fs && sim.requestFullscreen) { 305 | fs.onclick = function () { sim.requestFullscreen(); } 306 | } else if (fs) { 307 | fs.remove(); 308 | } 309 | } 310 | 311 | function postMessageToParentAsync(message: any) { 312 | return new Promise(resolve => { 313 | message.id = nextMessageId++; 314 | pendingMessages[message.id] = resolve; 315 | if ((window as any).acquireVsCodeApi) { 316 | if (!_vsapi) { 317 | _vsapi = (window as any).acquireVsCodeApi(); 318 | } 319 | _vsapi.postMessage(message); 320 | } 321 | else { 322 | window.postMessage(message); 323 | } 324 | }); 325 | } 326 | } 327 | -------------------------------------------------------------------------------- /packages/makecode-core/src/service.ts: -------------------------------------------------------------------------------- 1 | import vm = require("vm") 2 | import mkc = require("./mkc") 3 | import downloader = require("./downloader") 4 | import { host, LanguageService } from "./host"; 5 | 6 | const cdnUrl = "https://cdn.makecode.com" 7 | 8 | export interface HexInfo { 9 | hex: string[] 10 | } 11 | 12 | export interface ExtensionInfo { 13 | sha: string 14 | compileData: string 15 | skipCloudBuild?: boolean 16 | hexinfo?: HexInfo 17 | appVariant?: string 18 | } 19 | 20 | export interface ExtensionTarget { 21 | extinfo: ExtensionInfo 22 | // target: CompileTarget 23 | } 24 | 25 | export interface CompileOptions { 26 | fileSystem: pxt.Map 27 | testMode?: boolean 28 | sourceFiles?: string[] 29 | generatedFiles?: string[] 30 | jres?: pxt.Map 31 | noEmit?: boolean 32 | forceEmit?: boolean 33 | ast?: boolean 34 | breakpoints?: boolean 35 | trace?: boolean 36 | justMyCode?: boolean 37 | computeUsedSymbols?: boolean 38 | computeUsedParts?: boolean 39 | name?: string 40 | warnDiv?: boolean // warn when emitting division operator 41 | bannedCategories?: string[] 42 | skipPxtModulesTSC?: boolean // skip re-checking of pxt_modules/* 43 | skipPxtModulesEmit?: boolean // skip re-emit of pxt_modules/* 44 | embedMeta?: string 45 | embedBlob?: string // base64 46 | 47 | extinfo?: ExtensionInfo 48 | otherMultiVariants?: ExtensionTarget[] 49 | } 50 | 51 | export interface BuiltSimJsInfo { 52 | js: string 53 | targetVersion: string 54 | fnArgs?: pxt.Map 55 | parts?: string[] 56 | usedBuiltinParts?: string[] 57 | allParts?: string[] 58 | breakpoints?: number[] 59 | } 60 | 61 | export enum DiagnosticCategory { 62 | Warning = 0, 63 | Error = 1, 64 | Message = 2, 65 | } 66 | export interface LocationInfo { 67 | fileName: string 68 | start: number 69 | length: number 70 | line: number 71 | column: number 72 | endLine?: number 73 | endColumn?: number 74 | } 75 | export interface DiagnosticMessageChain { 76 | messageText: string 77 | category: DiagnosticCategory 78 | code: number 79 | next?: DiagnosticMessageChain 80 | } 81 | export interface KsDiagnostic extends LocationInfo { 82 | code: number 83 | category: DiagnosticCategory 84 | messageText: string | DiagnosticMessageChain 85 | } 86 | 87 | export interface CompileResult { 88 | outfiles: pxt.Map 89 | diagnostics: KsDiagnostic[] 90 | success: boolean 91 | times: pxt.Map 92 | // breakpoints?: Breakpoint[]; 93 | usedArguments?: pxt.Map 94 | usedParts?: string[] 95 | binaryPath?: string; 96 | simJsInfo?: BuiltSimJsInfo 97 | } 98 | 99 | export interface ServiceUser { 100 | linkedPackage: (id: string) => Promise> 101 | } 102 | 103 | interface SimpleDriverCallbacks { 104 | cacheGet: (key: string) => Promise 105 | cacheSet: (key: string, val: string) => Promise 106 | httpRequestAsync?: ( 107 | options: downloader.HttpRequestOptions 108 | ) => Promise 109 | pkgOverrideAsync?: (id: string) => Promise> 110 | } 111 | 112 | export class Ctx { 113 | lastUser: ServiceUser 114 | private makerHw = false 115 | supportsGhPkgs = false 116 | languageService: LanguageService; 117 | 118 | constructor(public editor: mkc.DownloadedEditor) { 119 | this.initAsync(); 120 | } 121 | 122 | async initAsync() { 123 | this.languageService = await host().createLanguageServiceAsync(this.editor); 124 | 125 | const cachePref = "c-" // TODO should this be editor-dependent? 126 | 127 | const callbacks: SimpleDriverCallbacks = { 128 | cacheGet: (key: string) => 129 | this.editor.cache 130 | .getAsync(cachePref + key) 131 | .then(buf => (buf ? host().bufferToString(buf) : null)), 132 | cacheSet: (key: string, val: string) => 133 | this.editor.cache.setAsync( 134 | cachePref + key, 135 | host().stringToBuffer(val) 136 | ), 137 | httpRequestAsync: (options: downloader.HttpRequestOptions) => 138 | host().requestAsync(options, (protocol, method) => { 139 | if (protocol != "https:") 140 | throw new Error("only https: supported") 141 | if (method != "GET") throw new Error("only GET supported") 142 | if (!options.url.startsWith(cdnUrl + "/") && !options.url.startsWith("https://www.makecode.com/api/")) 143 | throw new Error("only CDN URLs and makecode.com/api support: " + cdnUrl + ", got " + options.url) 144 | mkc.log("GET " + options.url) 145 | }), 146 | pkgOverrideAsync: id => { 147 | if (this.lastUser && this.lastUser.linkedPackage) 148 | return this.lastUser.linkedPackage(id) 149 | else return Promise.resolve(null) 150 | }, 151 | } 152 | 153 | await this.languageService.registerDriverCallbacksAsync(callbacks); 154 | await this.languageService.setWebConfigAsync({ 155 | cdnUrl: "https://cdn.makecode.com", 156 | } as downloader.WebConfig); 157 | this.supportsGhPkgs = await this.languageService.supportsGhPackagesAsync(); 158 | } 159 | 160 | async setUserAsync(user: ServiceUser) { 161 | if (this.lastUser !== user) { 162 | this.lastUser = user 163 | if (user) await this.languageService.performOperationAsync("reset", {}) 164 | } 165 | } 166 | 167 | private async compileExtInfo(extinfo: ExtensionInfo) { 168 | let existing = await this.editor.cache.getAsync("cpp-" + extinfo.sha) 169 | if (!existing) { 170 | const url = this.editor.cdnUrl + "/compile/" + extinfo.sha + ".hex" 171 | const resp = await downloader.requestAsync({ url }).then( 172 | r => r, 173 | err => null 174 | ) 175 | if (resp == null) { 176 | mkc.log(`compiling C++; this can take a while`) 177 | const cdata = extinfo.compileData 178 | const cdataObj: any = JSON.parse( 179 | host().bufferToString(host().stringToBuffer(cdata, "base64")) 180 | ) 181 | if (!cdataObj.config) 182 | throw new Error( 183 | `Compile config missing in C++; compile variant likely misconfigured` 184 | ) 185 | // writeFileSync("compilereq.json", JSON.stringify(JSON.parse(Buffer.from(cdata, "base64").toString()), null, 4)) 186 | const cresp = await downloader.requestAsync({ 187 | url: "https://www.makecode.com/api/compile/extension", 188 | data: { data: cdata }, 189 | allowGzipPost: true, 190 | }) 191 | const hexurl = cresp.json.hex 192 | const jsonUrl = hexurl.replace(/\.hex/, ".json") 193 | let ok = false 194 | for (let i = 0; i < 30; ++i) { 195 | const jresp = await downloader 196 | .requestAsync({ url: jsonUrl }) 197 | .then( 198 | r => r, 199 | e => null 200 | ) 201 | if (jresp) { 202 | const json = jresp.json 203 | mkc.log( 204 | `build log ${jsonUrl.replace(/\.json$/, ".log")}` 205 | ) 206 | if (!json.success) { 207 | mkc.error(`C++ build failed`) 208 | if ( 209 | json.mbedresponse && 210 | json.mbedresponse.result && 211 | json.mbedresponse.result.exception 212 | ) 213 | mkc.error(json.mbedresponse.result.exception) 214 | throw new Error("C++ build failed") 215 | } else { 216 | const hexresp = await downloader.requestAsync({ 217 | url: hexurl, 218 | }) 219 | ok = true 220 | existing = hexresp.buffer 221 | break 222 | } 223 | } 224 | // as in pxt CLI, wait for 8 seconds, then check again 225 | const delay = 8000 226 | mkc.log(`waiting ${(delay / 1000) | 0}s for C++ build... [${i}]`) 227 | await new Promise(resolve => setTimeout(resolve, delay)) 228 | } 229 | if (!ok) { 230 | mkc.error(`C++ build timed out`) 231 | throw new Error("C++ build timed out") 232 | } 233 | } else { 234 | existing = resp.buffer 235 | } 236 | await this.editor.cache.setAsync("cpp-" + extinfo.sha, existing) 237 | } 238 | extinfo.hexinfo = { hex: host().bufferToString(existing).split(/\r?\n/) } 239 | } 240 | 241 | async simpleCompileAsync( 242 | prj: mkc.Package, 243 | simpleOpts: any = {} 244 | ): Promise { 245 | let opts = await this.getOptionsAsync(prj, simpleOpts) 246 | 247 | if (simpleOpts.emitBreakpoints) { 248 | opts.breakpoints = true; 249 | } 250 | 251 | if (simpleOpts.native && opts?.extinfo?.sha) { 252 | const infos = [opts.extinfo].concat( 253 | (opts.otherMultiVariants || []).map(x => x.extinfo) 254 | ) 255 | for (const info of infos) await this.compileExtInfo(info) 256 | } 257 | 258 | // Manually set this option for now 259 | if (simpleOpts.computeUsedParts) opts.computeUsedParts = true 260 | 261 | // opts.breakpoints = true 262 | return this.languageService.performOperationAsync("compile", { options: opts }) 263 | } 264 | 265 | async buildSimJsInfoAsync(result: CompileResult): Promise { 266 | return await this.languageService.buildSimJsInfoAsync(result); 267 | } 268 | 269 | private async setHwVariantAsync(prj: mkc.Package) { 270 | if (this.makerHw) { 271 | const tmp = Object.assign({}, prj.files) 272 | const cfg: pxt.PackageConfig = JSON.parse(tmp["pxt.json"]) 273 | if (prj.mkcConfig.hwVariant) 274 | cfg.dependencies[prj.mkcConfig.hwVariant] = "*" 275 | tmp["pxt.json"] = mkc.stringifyConfig(cfg) 276 | await this.languageService.setProjectTextAsync(tmp); 277 | } else { 278 | await this.languageService.setProjectTextAsync(prj.files); 279 | await this.languageService.setHwVariantAsync(prj.mkcConfig.hwVariant || ""); 280 | } 281 | } 282 | 283 | async getOptionsAsync(prj: mkc.Package, simpleOpts: any = {}) { 284 | await this.setHwVariantAsync(prj); 285 | return this.languageService.getCompileOptionsAsync( 286 | prj, 287 | simpleOpts 288 | ) 289 | } 290 | 291 | async installGhPackagesAsync(prj: mkc.Package) { 292 | await this.setHwVariantAsync(prj); 293 | const pkg = await this.languageService.installGhPackagesAsync(prj.files); 294 | prj.files = pkg; 295 | } 296 | 297 | async getHardwareVariantsAsync() { 298 | let hwVariants = await this.languageService.getHardwareVariantsAsync(); 299 | if (hwVariants.length == 0) { 300 | hwVariants = await this.languageService.getBundledPackageConfigsAsync(); 301 | hwVariants = hwVariants.filter( 302 | pkg => !/prj/.test(pkg.name) && !!pkg.core 303 | ) 304 | for (const pkg of hwVariants) { 305 | pkg.card = { 306 | name: "", 307 | description: pkg.description, 308 | } 309 | } 310 | if (hwVariants.length > 1) this.makerHw = true 311 | else hwVariants = [] 312 | } 313 | 314 | return hwVariants 315 | } 316 | 317 | dispose() { 318 | this.languageService?.dispose?.(); 319 | } 320 | } 321 | -------------------------------------------------------------------------------- /packages/makecode-core/src/downloader.ts: -------------------------------------------------------------------------------- 1 | import * as mkc from "./mkc" 2 | 3 | import { host } from "./host"; 4 | 5 | import { DOMParser, Element, XMLSerializer } from "@xmldom/xmldom"; 6 | 7 | export interface HttpRequestOptions { 8 | url: string 9 | method?: string // default to GET 10 | data?: any 11 | headers?: pxt.Map 12 | allowHttpErrors?: boolean // don't treat non-200 responses as errors 13 | allowGzipPost?: boolean 14 | } 15 | 16 | export interface HttpResponse { 17 | statusCode: number 18 | headers: pxt.Map 19 | buffer?: any 20 | text?: string 21 | json?: any 22 | } 23 | 24 | export function requestAsync( 25 | options: HttpRequestOptions 26 | ): Promise { 27 | log("Download " + options.url) 28 | return host().requestAsync(options).then(resp => { 29 | if ( 30 | resp.statusCode != 200 && 31 | resp.statusCode != 304 && 32 | !options.allowHttpErrors 33 | ) { 34 | let msg = `Bad HTTP status code: ${resp.statusCode} at ${ 35 | options.url 36 | }; message: ${(resp.text || "").slice(0, 500)}` 37 | let err: any = new Error(msg) 38 | err.statusCode = resp.statusCode 39 | return Promise.reject(err) 40 | } 41 | if ( 42 | resp.text && 43 | /application\/json/.test(resp.headers["content-type"] as string) 44 | ) 45 | resp.json = JSON.parse(resp.text) 46 | return resp 47 | }) 48 | } 49 | 50 | export function httpGetTextAsync(url: string) { 51 | return requestAsync({ url: url }).then(resp => resp.text) 52 | } 53 | 54 | export function httpGetJsonAsync(url: string) { 55 | return requestAsync({ url: url }).then(resp => resp.json) 56 | } 57 | 58 | export interface WebConfig { 59 | relprefix: string // "/beta---", 60 | workerjs: string // "/beta---worker", 61 | monacoworkerjs: string // "/beta---monacoworker", 62 | gifworkerjs: string // /beta---gifworker", 63 | pxtVersion: string // "?", 64 | pxtRelId: string // "9e298e8784f1a1d6787428ec491baf1f7a53e8fa", 65 | pxtCdnUrl: string // "https://cdn.makecode.com/commit/9e2...e8fa/", 66 | commitCdnUrl: string // "https://cdn.makecode.com/commit/9e2...e8fa/", 67 | blobCdnUrl: string // "https://cdn.makecode.com/commit/9e2...e8fa/", 68 | cdnUrl: string // "https://cdn.makecode.com" 69 | targetUrl: string // "https://pxt.microbit.org" 70 | targetVersion: string // "?", 71 | targetRelId: string // "9e298e8784f1a1d6787428ec491baf1f7a53e8fa", 72 | targetId: string // "microbit", 73 | simUrl: string // "https://trg-microbit.userpxt.io/beta---simulator" 74 | partsUrl?: string // /beta---parts 75 | runUrl?: string // "/beta---run" 76 | docsUrl?: string // "/beta---docs" 77 | isStatic?: boolean 78 | verprefix?: string // "v1" 79 | 80 | // added here 81 | rootUrl: string 82 | manifestUrl?: string 83 | files: { [index: string]: string } 84 | } 85 | 86 | function resolveUrl(root: string, path: string) { 87 | if (path[0] == "/") { 88 | return root.replace(/(:\/\/[^\/]*)\/.*/, (x, y) => y) + path 89 | } 90 | return path 91 | } 92 | 93 | async function parseWebConfigAsync(url: string): Promise { 94 | // html 95 | const html: string = await httpGetTextAsync(url) 96 | 97 | const lines = html.split("\n"); 98 | 99 | let rawConfig = ""; 100 | let openBrackets = 0; 101 | for (const line of lines) { 102 | if (line.indexOf("var pxtConfig =") !== -1) { 103 | openBrackets++; 104 | rawConfig += line.slice(line.indexOf("{")); 105 | } 106 | else if (openBrackets) { 107 | if (line.indexOf("{") !== -1) { 108 | openBrackets++; 109 | } 110 | if (line.indexOf("}") !== -1) { 111 | openBrackets--; 112 | 113 | if (openBrackets === 0) { 114 | rawConfig += line.slice(0, line.indexOf("}") + 1); 115 | break; 116 | } 117 | } 118 | rawConfig += line; 119 | } 120 | } 121 | const config = rawConfig && (JSON.parse(rawConfig) as WebConfig) 122 | if (config) { 123 | config.rootUrl = url 124 | config.files = {} 125 | 126 | const m = /manifest="([^"]+)"/.exec(html) 127 | if (m) config.manifestUrl = resolveUrl(url, m[1]) 128 | } 129 | return config 130 | } 131 | 132 | export interface DownloadInfo { 133 | manifestUrl?: string 134 | manifest?: string 135 | manifestEtag?: string 136 | cdnUrl?: string 137 | simKey?: string 138 | assetEditorKey?: string; 139 | versionNumber?: number 140 | updateCheckedAt?: number 141 | webConfig?: WebConfig 142 | targetVersion?: string 143 | targetConfig?: any // see pxt/localtypings/pxtarget.d.ts interface TargetConfig 144 | 145 | cachedAssetEditorKey?: string; 146 | cachedSimulatorKey?: string; 147 | } 148 | 149 | function log(msg: string) { 150 | console.log(msg) 151 | } 152 | 153 | export async function downloadAsync( 154 | cache: mkc.Cache, 155 | webAppUrl: string, 156 | forceCheckUpdates = false 157 | ) { 158 | const infoBuf = await cache.getAsync(webAppUrl + "-info") 159 | const info: DownloadInfo = infoBuf 160 | ? JSON.parse(host().bufferToString(infoBuf)) 161 | : {} 162 | const fetchTargetConfig = async (cdnUrl: string, target: string, version: string) => { 163 | const currentDate = new Date(); 164 | const year = currentDate.getUTCFullYear(); 165 | const month = `${currentDate.getUTCMonth()}`.padStart(2, "0"); 166 | const day = `${currentDate.getUTCDay()}`.padStart(2, "0"); 167 | const cacheBustId = `${year}${month}${day}`; 168 | const resp = await requestAsync({ 169 | url: `${cdnUrl}/api/config/${target}/targetconfig${version ? `/v${version}`: ""}?cdn=${cacheBustId}` 170 | }); 171 | return JSON.parse(resp.text); 172 | } 173 | 174 | if (forceCheckUpdates && info.manifest && info.webConfig) { 175 | let needsUpdate = false 176 | if ( 177 | !info.updateCheckedAt || 178 | Date.now() - info.updateCheckedAt > 24 * 3600 * 1000 179 | ) { 180 | info.updateCheckedAt = Date.now() 181 | await saveInfoAsync() // save last check time *before* checking - in case user hits ctrl-c we don't want another build to hang again 182 | try { 183 | log("Checking for updates (only happens once daily)...") 184 | needsUpdate = await hasNewManifestAsync() 185 | if (!needsUpdate) { 186 | // fetch new target config as that is 'live' 187 | const targetConfig = await fetchTargetConfig( 188 | info.webConfig.cdnUrl, 189 | info.webConfig.targetId, 190 | info.targetVersion 191 | ); 192 | if (targetConfig) { 193 | info.targetConfig = targetConfig; 194 | await saveInfoAsync(); 195 | } 196 | } 197 | } catch (e) { 198 | log( 199 | `Error checking for updates; will try again tomorrow (use -u flag to force); ${e.message}` 200 | ) 201 | } 202 | } 203 | if (!needsUpdate) return loadFromCacheAsync() 204 | } else { 205 | if (!(await hasNewManifestAsync())) return loadFromCacheAsync(); 206 | } 207 | 208 | log("Download new webapp") 209 | const cfg = await parseWebConfigAsync(webAppUrl) 210 | if (!cfg.manifestUrl) cfg.manifestUrl = webAppUrl // use index.html if no manifest 211 | if (info.manifestUrl != cfg.manifestUrl || !info.webConfig) { 212 | info.manifestUrl = cfg.manifestUrl 213 | info.manifestEtag = null 214 | info.cdnUrl = cfg.cdnUrl 215 | info.webConfig = cfg 216 | await hasNewManifestAsync() 217 | } 218 | info.versionNumber = (info.versionNumber || 0) + 1 219 | info.updateCheckedAt = Date.now() 220 | 221 | await saveFileAsync("pxtworker.js"); 222 | const targetJsonBuf = await saveFileAsync("target.json"); 223 | const targetJson = JSON.parse( 224 | host().bufferToString(targetJsonBuf) 225 | ); 226 | info.targetVersion = targetJson?.versions?.target; 227 | 228 | const targetConfig = await fetchTargetConfig( 229 | cfg.cdnUrl, 230 | cfg.targetId, 231 | info.targetVersion 232 | ); 233 | info.targetConfig = targetConfig; 234 | 235 | if (cache.rootPath) { 236 | info.simKey = webAppUrl + "-sim.html" 237 | await downloadPageAndDependenciesAsync(cfg.simUrl, info.simKey); 238 | 239 | info.assetEditorKey = webAppUrl + "-asseteditor.html"; 240 | await downloadPageAndDependenciesAsync(webAppUrl + "---asseteditor", info.assetEditorKey); 241 | } 242 | 243 | delete info.cachedAssetEditorKey; 244 | delete info.cachedSimulatorKey; 245 | 246 | return loadFromCacheAsync() 247 | 248 | function saveInfoAsync() { 249 | return cache.setAsync( 250 | webAppUrl + "-info", 251 | host().stringToBuffer(JSON.stringify(info)) 252 | ) 253 | } 254 | 255 | async function loadFromCacheAsync() { 256 | await saveInfoAsync() 257 | const res: mkc.DownloadedEditor = { 258 | cache, 259 | versionNumber: info.versionNumber || 0, 260 | cdnUrl: info.cdnUrl, 261 | website: webAppUrl, 262 | simUrl: info.simKey 263 | ? cache.rootPath + "/" + cache.expandKey(info.simKey) 264 | : null, 265 | pxtWorkerJs: host().bufferToString( 266 | await cache.getAsync(webAppUrl + "-pxtworker.js") 267 | ), 268 | targetJson: JSON.parse( 269 | host().bufferToString(await cache.getAsync(webAppUrl + "-target.json")) 270 | ), 271 | webConfig: info.webConfig, 272 | targetConfig: info.targetConfig 273 | } 274 | return res 275 | } 276 | 277 | async function saveFileAsync(name: string) { 278 | const resp = await requestAsync({ url: cfg.pxtCdnUrl + name }) 279 | await cache.setAsync(webAppUrl + "-" + name, resp.buffer) 280 | return resp.buffer; 281 | } 282 | 283 | async function hasNewManifestAsync() { 284 | if (!info.manifestUrl || !info.webConfig) return true 285 | 286 | const resp = await requestAsync({ 287 | url: info.manifestUrl, 288 | headers: info.manifestEtag 289 | ? { 290 | "if-none-match": info.manifestEtag, 291 | } 292 | : {}, 293 | }) 294 | 295 | if (resp.statusCode == 304) { 296 | info.updateCheckedAt = Date.now() 297 | return false 298 | } 299 | 300 | info.manifestEtag = resp.headers["etag"] as string 301 | if (resp.text == info.manifest) { 302 | info.updateCheckedAt = Date.now() 303 | return false 304 | } 305 | 306 | info.manifest = resp.text 307 | return true 308 | } 309 | 310 | async function downloadPageAndDependenciesAsync(url: string, cacheKey: string) { 311 | let pageText = await httpGetTextAsync(url); 312 | 313 | const dom = new DOMParser().parseFromString(pageText, "text/html"); 314 | 315 | const additionalUrls: string[] = [] 316 | const urlKeyMap: pxt.Map = {} 317 | 318 | for (const script of dom.getElementsByTagName("script")) { 319 | if (!script.hasAttribute("src")) continue; 320 | 321 | const url = script.getAttribute("src"); 322 | if (!url.startsWith(info.cdnUrl) || !url.endsWith(".js")) continue; 323 | 324 | additionalUrls.push(url); 325 | urlKeyMap[url] = webAppUrl + "-" + url.replace(/.*\//, ""); 326 | script.setAttribute("src", cache.expandKey(urlKeyMap[url])); 327 | } 328 | 329 | for (const link of dom.getElementsByTagName("link")) { 330 | if (!link.hasAttribute("href")) continue; 331 | 332 | const url = link.getAttribute("href"); 333 | if (!url.startsWith(info.cdnUrl) || !url.endsWith(".css")) continue; 334 | 335 | additionalUrls.push(url); 336 | urlKeyMap[url] = webAppUrl + "-" + url.replace(/.*\//, ""); 337 | link.setAttribute("href", cache.expandKey(urlKeyMap[url])); 338 | } 339 | 340 | pageText = new XMLSerializer().serializeToString(dom); 341 | pageText = pageText.replace(/ manifest=/, " x-manifest=") 342 | 343 | await cache.setAsync(cacheKey, host().stringToBuffer(pageText)) 344 | for (let url of additionalUrls) { 345 | const resp = await requestAsync({ url }) 346 | await cache.setAsync(urlKeyMap[url], resp.buffer) 347 | } 348 | } 349 | } 350 | -------------------------------------------------------------------------------- /packages/makecode-core/src/mkc.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | export import downloader = require("./downloader") 4 | export import files = require("./files") 5 | export import service = require("./service") 6 | export import loader = require("./loader") 7 | import { collectCurrentVersionAsync, monoRepoConfigsAsync } from "./files" 8 | 9 | export interface MkcJson { 10 | targetWebsite: string 11 | hwVariant?: string 12 | links?: pxt.Map 13 | overrides?: Partial 14 | include?: string[] 15 | } 16 | 17 | export interface Cache { 18 | getAsync(key: string): Promise 19 | setAsync(key: string, val: Uint8Array): Promise 20 | expandKey?(key: string): string 21 | rootPath?: string 22 | } 23 | 24 | export interface DownloadedEditor { 25 | cache: Cache 26 | versionNumber: number 27 | cdnUrl: string 28 | simUrl: string 29 | website: string 30 | pxtWorkerJs: string 31 | targetJson: any 32 | webConfig: downloader.WebConfig, 33 | targetConfig: any 34 | } 35 | 36 | export interface Package { 37 | config: pxt.PackageConfig 38 | mkcConfig: MkcJson 39 | files: pxt.Map 40 | fromTargetJson?: boolean 41 | } 42 | 43 | export interface Workspace { 44 | packages: pxt.Map 45 | } 46 | 47 | export let cloudRoot = "https://makecode.com/api/" 48 | 49 | function jsonCopyFrom(trg: T, src: T) { 50 | let v = JSON.parse(JSON.stringify(src)) 51 | for (let k of Object.keys(src)) { 52 | ; (trg as any)[k] = (v as any)[k] 53 | } 54 | } 55 | 56 | export class Project { 57 | editor: DownloadedEditor 58 | service: service.Ctx 59 | mainPkg: Package 60 | lastPxtJson: string 61 | private _hwVariant: string 62 | writePxtModules = true 63 | linkPxtModules = false 64 | symlinkPxtModules = false 65 | outputPrefix = "built" 66 | mkcConfig: MkcJson 67 | 68 | constructor(public directory: string, protected cache: Cache = null) { 69 | } 70 | 71 | get hwVariant() { 72 | return this._hwVariant 73 | } 74 | set hwVariant(value: string) { 75 | this._hwVariant = value 76 | if (this.mainPkg) this.mainPkg.mkcConfig.hwVariant = value 77 | } 78 | 79 | async guessHwVariantAsync() { 80 | if (this.mainPkg.mkcConfig.hwVariant) return 81 | 82 | const variants = await this.service.getHardwareVariantsAsync(); 83 | const cfg = this.mainPkg.config 84 | for (const v of variants) { 85 | if (cfg.dependencies[v.name] || cfg.testDependencies?.[v.name]) { 86 | log("guessing hw-variant: " + hwid(v)) 87 | this.hwVariant = hwid(v) 88 | return 89 | } 90 | } 91 | 92 | log("selecting first hw-variant: " + hwid(variants[0])) 93 | this.hwVariant = hwid(variants[0]) 94 | 95 | function hwid(cfg: pxt.PackageConfig) { 96 | return cfg.name.replace(/hw---/, "") 97 | } 98 | } 99 | 100 | protected readFileAsync(filename: string) { 101 | return files.readPrjFileAsync(this.directory, filename) 102 | } 103 | 104 | protected saveBuiltFilesAsync(res: service.CompileResult) { 105 | return files.saveBuiltFilesAsync(this.directory, res, this.outputPrefix) 106 | } 107 | 108 | protected async savePxtModulesAsync(filesmap0: pxt.Map) { 109 | let filesmap: pxt.Map = filesmap0 110 | if (this.linkPxtModules || this.symlinkPxtModules) { 111 | let libsPath = await files.findParentDirWithAsync("..", "pxtarget.json") 112 | if (libsPath) 113 | libsPath = 114 | files.relativePath(".", libsPath).replace(/\\/g, "/") + 115 | "/libs" 116 | filesmap = JSON.parse(JSON.stringify(filesmap0)) 117 | const pxtmod = "pxt_modules/" 118 | const filesByPkg: pxt.Map = {} 119 | const filenames = Object.keys(filesmap) 120 | for (const s of filenames) { 121 | if (s.startsWith(pxtmod)) { 122 | const id = s.slice(pxtmod.length).replace(/\/.*/, "") 123 | if (!filesByPkg[id]) filesByPkg[id] = [] 124 | filesByPkg[id].push(s) 125 | } 126 | } 127 | for (const id of Object.keys(filesByPkg)) { 128 | let lnk = this.mkcConfig.links?.[id] 129 | let rel = "" 130 | if (lnk) 131 | rel = files.relativePath( 132 | this.directory + "/pxt_modules/foobar", 133 | lnk 134 | ) 135 | else if (await files.fileExistsAsync(`${libsPath}/${id}/pxt.json`)) { 136 | lnk = `${libsPath}/${id}` 137 | rel = `../../${lnk}` 138 | } 139 | if (lnk && this.linkPxtModules) { 140 | for (const fn of filesByPkg[id]) delete filesmap[fn] 141 | log(`link ${id} -> ${lnk}`) 142 | const pxtJson = JSON.stringify( 143 | { 144 | additionalFilePath: rel, 145 | }, 146 | null, 147 | 4 148 | ) 149 | filesmap["pxt_modules/" + id + "/pxt.json"] = pxtJson 150 | if (/---/.test(id)) { 151 | filesmap[ 152 | "pxt_modules/" + 153 | id.replace(/---.*/, "") + 154 | "/pxt.json" 155 | ] = pxtJson 156 | } 157 | } else if (lnk && this.symlinkPxtModules) { 158 | for (const fn of filesByPkg[id]) { 159 | const bn = fn.replace(/.*\//, "") 160 | if (await files.fileExistsAsync(`${lnk}/${bn}`)) { 161 | filesmap[fn] = { symlink: `${rel}/${bn}` } 162 | // log(`symlink ${fn} -> ${rel}/${bn}`) 163 | } else { 164 | log(`not link ${fn}`) 165 | } 166 | } 167 | } 168 | } 169 | } 170 | return files.savePxtModulesAsync(this.directory, filesmap) 171 | } 172 | 173 | async readPxtConfig() { 174 | const pxtJson = await this.readFileAsync("pxt.json") 175 | return JSON.parse(pxtJson) as pxt.PackageConfig 176 | } 177 | 178 | protected async readPackageAsync() { 179 | if (!this.mkcConfig) 180 | this.mkcConfig = JSON.parse( 181 | await this.readFileAsync("mkc.json").then( 182 | s => s, 183 | _err => "{}" 184 | ) 185 | ) 186 | const res: Package = { 187 | config: await this.readPxtConfig(), 188 | mkcConfig: this.mkcConfig, 189 | files: {}, 190 | } 191 | if (res.mkcConfig.overrides) 192 | jsonCopyFrom(res.config, res.mkcConfig.overrides) 193 | res.files["pxt.json"] = stringifyConfig(res.config) 194 | for (let f of res.config.files.concat(res.config.testFiles || [])) { 195 | res.files[f] = await this.readFileAsync(f) 196 | } 197 | if (res.files["main.ts"] === undefined) res.files["main.ts"] = "" // avoid bogus warning from PXT 198 | return res 199 | } 200 | 201 | async loadPkgAsync() { 202 | if (this.mainPkg) return 203 | 204 | const prj = await this.readPackageAsync() 205 | loader.guessMkcJson(prj) 206 | 207 | if (this.hwVariant) prj.mkcConfig.hwVariant = this.hwVariant 208 | 209 | // TODO handle require("lzma") in worker 210 | prj.config.binaryonly = true 211 | const pxtJson = (prj.files["pxt.json"] = stringifyConfig(prj.config)) 212 | 213 | this.mainPkg = prj 214 | 215 | if (pxtJson != this.lastPxtJson) { 216 | this.lastPxtJson = pxtJson 217 | if (this.service) await this.service.setUserAsync(null) 218 | } 219 | } 220 | 221 | updateEditorAsync() { 222 | return this.loadEditorAsync(true) 223 | } 224 | 225 | async loadEditorAsync(forceUpdate = false) { 226 | if (this.editor && !forceUpdate) return false 227 | 228 | await this.loadPkgAsync() 229 | 230 | const newEditor = await downloader.downloadAsync( 231 | await this.getCacheAsync(), 232 | this.mainPkg.mkcConfig.targetWebsite, 233 | !forceUpdate 234 | ) 235 | 236 | if ( 237 | !this.editor || 238 | newEditor.versionNumber != this.editor.versionNumber 239 | ) { 240 | this.editor = newEditor 241 | if (this.service) { 242 | this.service.dispose(); 243 | } 244 | this.service = new service.Ctx(this.editor) 245 | return true 246 | } else { 247 | return false 248 | } 249 | } 250 | 251 | async getCacheAsync() { 252 | if (!this.cache) this.cache = await files.mkHomeCacheAsync(); 253 | return this.cache; 254 | } 255 | 256 | async maybeWritePxtModulesAsync() { 257 | await this.loadEditorAsync() 258 | await this.loadPkgAsync() 259 | 260 | const wasThis = this.service.lastUser == this 261 | 262 | this.service.setUserAsync(this) 263 | 264 | if (this.service.supportsGhPkgs) { 265 | await this.service.installGhPackagesAsync(this.mainPkg) 266 | } else { 267 | await loader.loadDeps(this.editor, this.mainPkg) 268 | } 269 | 270 | if (this.writePxtModules && !wasThis) { 271 | log("writing pxt_modules/*") 272 | await this.savePxtModulesAsync(this.mainPkg.files) 273 | } 274 | } 275 | 276 | async linkedPackage(id: string) { 277 | const folder = this.mainPkg?.mkcConfig?.links?.[id] 278 | if (folder) return files.readProjectAsync(folder) 279 | return null 280 | } 281 | 282 | async buildAsync(simpleOpts = {}) { 283 | this.mainPkg = null // force reload 284 | 285 | await this.maybeWritePxtModulesAsync() 286 | 287 | await this.service.setUserAsync(this) 288 | const res = await this.service.simpleCompileAsync( 289 | this.mainPkg, 290 | simpleOpts 291 | ) 292 | 293 | const err = (res as any).errorMessage 294 | if (err) throw new Error(err) 295 | 296 | const binjs = "binary.js" 297 | if (res.outfiles[binjs]) { 298 | const appTarget = await this.service.languageService.getAppTargetAsync(); 299 | const boardDef = appTarget.simulator?.boardDefinition 300 | if (boardDef) { 301 | res.outfiles[binjs] = 302 | `// boardDefinition=${JSON.stringify(boardDef)}\n` + 303 | res.outfiles[binjs] 304 | } 305 | const webConfig: downloader.WebConfig = 306 | this.editor.webConfig || (await this.service.languageService.getWebConfigAsync()) 307 | const configs = await monoRepoConfigsAsync(this.directory, true) 308 | const version = `v${await collectCurrentVersionAsync(configs) || "0"}` 309 | const meta: any = { 310 | simUrl: webConfig.simUrl, 311 | cdnUrl: webConfig.cdnUrl, 312 | version, 313 | target: appTarget.id, 314 | targetVersion: appTarget.versions.target, 315 | } 316 | 317 | res.outfiles[binjs] = 318 | `// meta=${JSON.stringify(meta)}\n` + res.outfiles[binjs] 319 | } 320 | 321 | await this.saveBuiltFilesAsync(res) 322 | 323 | //delete res.outfiles 324 | //delete (res as any).procDebugInfo 325 | //console.log(res) 326 | 327 | return res 328 | } 329 | 330 | async buildSimJsInfoAsync(result: service.CompileResult) { 331 | return await this.service.buildSimJsInfoAsync(result) 332 | } 333 | 334 | async mkChildProjectAsync(folder: string) { 335 | const prj = new Project(folder, await this.getCacheAsync()) 336 | prj.service = this.service 337 | prj.mkcConfig = this.mkcConfig 338 | if (this._hwVariant) prj.hwVariant = this._hwVariant 339 | prj.outputPrefix = this.outputPrefix 340 | prj.writePxtModules = this.writePxtModules 341 | prj.editor = this.editor 342 | return prj 343 | } 344 | } 345 | 346 | export let log = (msg: string) => { 347 | console.log(msg) 348 | } 349 | export let error = (msg: string) => { 350 | console.error(msg) 351 | } 352 | export let debug = (msg: string) => { 353 | console.debug(msg) 354 | } 355 | 356 | export function setLogging(fns: { 357 | log: (msg: string) => void 358 | error: (msg: string) => void 359 | debug: (msg: string) => void 360 | }) { 361 | log = fns.log 362 | error = fns.error 363 | debug = fns.debug 364 | } 365 | 366 | export function stringifyConfig(cfg: pxt.PackageConfig | MkcJson) { 367 | return JSON.stringify(cfg, null, 4) + "\n" 368 | } 369 | --------------------------------------------------------------------------------