pxt-core 13.2.9 → 13.2.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/built/cli.js CHANGED
@@ -18,6 +18,7 @@ const chalk = require('chalk');
18
18
  var U = pxt.Util;
19
19
  var Cloud = pxt.Cloud;
20
20
  const server = require("./server");
21
+ const localrepos = require("./localrepos");
21
22
  const build = require("./buildengine");
22
23
  const commandParser = require("./commandparser");
23
24
  const hid = require("./hid");
@@ -2749,6 +2750,9 @@ function serveAsync(parsed) {
2749
2750
  saveConfig();
2750
2751
  }
2751
2752
  let localToken = exports.globalConfig.localToken;
2753
+ // --local-repo directories resolve against the directory serve was started from,
2754
+ // which differs from the target root when serve moves into a target found below
2755
+ const invocationDir = process.cwd();
2752
2756
  if (!fs.existsSync("pxtarget.json")) {
2753
2757
  //Specifically when the target is being used as a library
2754
2758
  let targetDepLoc = nodeutil.targetDir;
@@ -2767,6 +2771,8 @@ function serveAsync(parsed) {
2767
2771
  }
2768
2772
  }
2769
2773
  }
2774
+ const targetDir = process.cwd();
2775
+ const localRepos = localrepos.loadLocalRepos(targetDir, parsed.flags["localRepo"] || [], invocationDir);
2770
2776
  return (justServe ? Promise.resolve() : buildAndWatchTargetAsync(includeSourceMaps, rebundle))
2771
2777
  .then(() => server.serveAsync({
2772
2778
  autoStart: !exports.globalConfig.noAutoStart,
@@ -2780,6 +2786,7 @@ function serveAsync(parsed) {
2780
2786
  noauth: parsed.flags["noauth"] || false,
2781
2787
  backport: parsed.flags["backport"] || 0,
2782
2788
  https: parsed.flags["https"] || false,
2789
+ localRepos,
2783
2790
  }));
2784
2791
  }
2785
2792
  exports.serveAsync = serveAsync;
@@ -6763,7 +6770,13 @@ ${pxt.crowdin.KEY_VARIABLE} - crowdin key
6763
6770
  argument: "backport",
6764
6771
  type: "number",
6765
6772
  },
6766
- https: { description: "use https protocol instead of http" }
6773
+ https: { description: "use https protocol instead of http" },
6774
+ localRepo: {
6775
+ description: "serve a GitHub repository from a local directory, as owner/repo=dir; repeatable, and applied over the entries of pxtlocal.json",
6776
+ aliases: ["local-repo"],
6777
+ argument: "repo",
6778
+ repeatable: true
6779
+ }
6767
6780
  }
6768
6781
  }, serveAsync);
6769
6782
  p.defineCommand({
@@ -7,6 +7,12 @@ export interface CommandFlag {
7
7
  possibleValues?: (string | RegExp)[];
8
8
  deprecated?: boolean;
9
9
  hidden?: boolean;
10
+ /**
11
+ * The flag may be given more than once, and its values are collected into a
12
+ * string[]. Applies to string flags with an argument only: defineCommand
13
+ * throws on a repeatable flag of another type or without an argument.
14
+ */
15
+ repeatable?: boolean;
10
16
  }
11
17
  export interface Command {
12
18
  name: string;
@@ -30,7 +36,7 @@ export interface ParsedCommand {
30
36
  name: string;
31
37
  args: string[];
32
38
  flags: {
33
- [index: string]: boolean | string | number;
39
+ [index: string]: boolean | string | number | string[];
34
40
  };
35
41
  }
36
42
  export declare class CommandParser {
@@ -13,6 +13,9 @@ class CommandParser {
13
13
  const aliasMap = {};
14
14
  for (const flag in c.flags) {
15
15
  const def = c.flags[flag];
16
+ if (def.repeatable && ((def.type && def.type !== "string") || !def.argument)) {
17
+ throw new Error(`Flag ${flag} of command ${c.name} is repeatable, so it must be a string flag with an argument`);
18
+ }
16
19
  recordAlias(flag, flag);
17
20
  const aliases = c.flags[flag].aliases;
18
21
  if (aliases) {
@@ -83,7 +86,10 @@ class CommandParser {
83
86
  if (currentFlagDef.possibleValues && currentFlagDef.possibleValues.length && !nodeutil.matchesAny(match[2], currentFlagDef.possibleValues)) {
84
87
  pxt.U.userError(`Unknown value for flag '${currentFlag}', '${match[2]}'`);
85
88
  }
86
- if (!currentFlagDef.type || currentFlagDef.type === "string") {
89
+ if (currentFlagDef.repeatable) {
90
+ flags[currentFlag] = (flags[currentFlag] || []).concat([match[2]]);
91
+ }
92
+ else if (!currentFlagDef.type || currentFlagDef.type === "string") {
87
93
  flags[currentFlag] = match[2];
88
94
  }
89
95
  else if (currentFlagDef.type === "boolean") {
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Name of the machine-local file, at the root of a target checkout, that maps
3
+ * GitHub repositories to local directories for `pxt serve`. It holds paths of
4
+ * the developer's machine, so it is meant to stay out of source control.
5
+ */
6
+ export declare const LOCAL_CONFIG_NAME = "pxtlocal.json";
7
+ /**
8
+ * Contents of pxtlocal.json.
9
+ */
10
+ export interface LocalConfig {
11
+ /**
12
+ * Maps a GitHub repository ("owner/repo") to the local directory holding
13
+ * its checkout, absolute or relative to the directory of pxtlocal.json.
14
+ */
15
+ localRepos?: pxt.Map<string>;
16
+ }
17
+ /**
18
+ * Repository metadata, in the shape the MakeCode GitHub proxy returns it.
19
+ */
20
+ export interface LocalRepoMeta {
21
+ name: string;
22
+ description: string;
23
+ }
24
+ /**
25
+ * Refs listing, in the shape the MakeCode GitHub proxy returns it.
26
+ */
27
+ export interface LocalRepoRefs {
28
+ refs: pxt.Map<string>;
29
+ }
30
+ /**
31
+ * Builds the map of GitHub repositories that `pxt serve` resolves from local
32
+ * directories.
33
+ *
34
+ * Entries come from pxtlocal.json in `targetDir`, when the file exists, with
35
+ * relative directories resolved against `targetDir`. Each `flagValue`
36
+ * ("owner/repo=dir", relative directories resolved against `cwd`) is applied
37
+ * after the file and replaces a file entry for the same repository.
38
+ *
39
+ * Returns the map from lowercase "owner/repo" to absolute directory. Throws a
40
+ * user error when pxtlocal.json is not valid JSON, or an entry names
41
+ * something other than a repository or a directory that does not exist.
42
+ */
43
+ export declare function loadLocalRepos(targetDir: string, flagValues: string[], cwd: string): pxt.Map<string>;
44
+ /**
45
+ * Answers a request for an aliased repository in the shape the MakeCode
46
+ * GitHub proxy answers it, reading the repository's local directory. The
47
+ * request is the path after "gh/":
48
+ *
49
+ * - "owner/repo": the repository metadata ({@link LocalRepoMeta}); the
50
+ * description is the one in the repository's root pxt.json, if any
51
+ * - "owner/repo/refs": the refs listing ({@link LocalRepoRefs}), always
52
+ * empty, because an aliased repository serves its working tree whatever
53
+ * ref is asked for
54
+ * - "owner/repo[/path]/text": the files of the package at `path` (the
55
+ * repository root when omitted), keyed by file name: its pxt.json and
56
+ * every file that pxt.json lists
57
+ *
58
+ * Throws an error carrying the HTTP `statusCode` to answer with: 404 when
59
+ * the repository is not aliased, or when the package's pxt.json or a file it
60
+ * lists does not exist; 400 when the path lies outside the repository's
61
+ * directory, the pxt.json is not valid or lists a file outside the package's
62
+ * directory, or the request matches none of the forms above. Every error is
63
+ * also written to the console.
64
+ */
65
+ export declare function resolveLocalRepoRequest(localRepos: pxt.Map<string>, request: string): LocalRepoMeta | LocalRepoRefs | pxt.Map<string>;
@@ -0,0 +1,169 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.resolveLocalRepoRequest = exports.loadLocalRepos = exports.LOCAL_CONFIG_NAME = void 0;
4
+ const fs = require("fs");
5
+ const path = require("path");
6
+ /**
7
+ * Name of the machine-local file, at the root of a target checkout, that maps
8
+ * GitHub repositories to local directories for `pxt serve`. It holds paths of
9
+ * the developer's machine, so it is meant to stay out of source control.
10
+ */
11
+ exports.LOCAL_CONFIG_NAME = "pxtlocal.json";
12
+ function requestError(statusCode, msg) {
13
+ const err = new Error(msg);
14
+ err.statusCode = statusCode;
15
+ return err;
16
+ }
17
+ function isDirectory(dir) {
18
+ try {
19
+ return fs.statSync(dir).isDirectory();
20
+ }
21
+ catch (e) {
22
+ return false;
23
+ }
24
+ }
25
+ function readTextOpt(fn) {
26
+ try {
27
+ return fs.readFileSync(fn, "utf8");
28
+ }
29
+ catch (e) {
30
+ if (e.code == "ENOENT" || e.code == "ENOTDIR")
31
+ return undefined;
32
+ throw e;
33
+ }
34
+ }
35
+ /**
36
+ * Resolves `rel` against the absolute directory `base`, and returns the
37
+ * resolved path if it is `base` or lies inside it, undefined otherwise.
38
+ */
39
+ function resolveInside(base, rel) {
40
+ const resolved = path.resolve(base, rel);
41
+ const prefix = base.endsWith(path.sep) ? base : base + path.sep;
42
+ return resolved == base || resolved.startsWith(prefix) ? resolved : undefined;
43
+ }
44
+ function addLocalRepo(localRepos, slug, dir, baseDir, source) {
45
+ const repo = pxt.github.parseRepoId(slug);
46
+ if (!repo || repo.fileName || repo.tag !== undefined || repo.slug != slug.trim())
47
+ pxt.U.userError(`${source}: '${slug}' is not a GitHub repository; expected owner/repo`);
48
+ if (!dir)
49
+ pxt.U.userError(`${source}: no directory given for ${slug}`);
50
+ const resolved = path.resolve(baseDir, dir);
51
+ if (!isDirectory(resolved))
52
+ pxt.U.userError(`${source}: directory ${resolved} for ${slug} does not exist`);
53
+ localRepos[repo.slug.toLowerCase()] = resolved;
54
+ }
55
+ /**
56
+ * Builds the map of GitHub repositories that `pxt serve` resolves from local
57
+ * directories.
58
+ *
59
+ * Entries come from pxtlocal.json in `targetDir`, when the file exists, with
60
+ * relative directories resolved against `targetDir`. Each `flagValue`
61
+ * ("owner/repo=dir", relative directories resolved against `cwd`) is applied
62
+ * after the file and replaces a file entry for the same repository.
63
+ *
64
+ * Returns the map from lowercase "owner/repo" to absolute directory. Throws a
65
+ * user error when pxtlocal.json is not valid JSON, or an entry names
66
+ * something other than a repository or a directory that does not exist.
67
+ */
68
+ function loadLocalRepos(targetDir, flagValues, cwd) {
69
+ const localRepos = {};
70
+ const configPath = path.join(targetDir, exports.LOCAL_CONFIG_NAME);
71
+ const configText = readTextOpt(configPath);
72
+ if (configText !== undefined) {
73
+ const config = pxt.U.jsonTryParse(configText);
74
+ if (!config)
75
+ pxt.U.userError(`${configPath}: invalid JSON`);
76
+ const entries = config.localRepos || {};
77
+ for (const slug of Object.keys(entries))
78
+ addLocalRepo(localRepos, slug, entries[slug], targetDir, configPath);
79
+ }
80
+ for (const flagValue of flagValues) {
81
+ const m = /^([^=]+)=(.*)$/.exec(flagValue);
82
+ if (!m)
83
+ pxt.U.userError(`--local-repo ${flagValue}: expected owner/repo=directory`);
84
+ addLocalRepo(localRepos, m[1], m[2], cwd, `--local-repo ${flagValue}`);
85
+ }
86
+ return localRepos;
87
+ }
88
+ exports.loadLocalRepos = loadLocalRepos;
89
+ function readPackageFiles(fullName, root, fileName) {
90
+ const dir = resolveInside(root, fileName);
91
+ if (!dir)
92
+ throw requestError(400, `${fullName}: the path is outside the repository`);
93
+ pxt.debug(`local repo: ${fullName} from ${dir}`);
94
+ const configPath = path.join(dir, pxt.CONFIG_NAME);
95
+ const configText = readTextOpt(configPath);
96
+ if (configText === undefined)
97
+ throw requestError(404, `${fullName}: ${configPath} does not exist (the repository is served from ${root})`);
98
+ const config = pxt.Package.parseAndValidConfig(configText);
99
+ if (!config)
100
+ throw requestError(400, `${fullName}: ${configPath} is not a valid ${pxt.CONFIG_NAME}`);
101
+ const files = {};
102
+ files[pxt.CONFIG_NAME] = configText;
103
+ for (const fn of pxt.allPkgFiles(config).slice(1)) {
104
+ const filePath = resolveInside(dir, fn);
105
+ if (!filePath)
106
+ throw requestError(400, `${fullName}: ${configPath} lists ${fn}, which is outside the package directory`);
107
+ const text = readTextOpt(filePath);
108
+ if (text === undefined)
109
+ throw requestError(404, `${fullName}: ${filePath}, listed in ${configPath}, does not exist`);
110
+ files[fn] = text;
111
+ }
112
+ return files;
113
+ }
114
+ /**
115
+ * Answers a request for an aliased repository in the shape the MakeCode
116
+ * GitHub proxy answers it, reading the repository's local directory. The
117
+ * request is the path after "gh/":
118
+ *
119
+ * - "owner/repo": the repository metadata ({@link LocalRepoMeta}); the
120
+ * description is the one in the repository's root pxt.json, if any
121
+ * - "owner/repo/refs": the refs listing ({@link LocalRepoRefs}), always
122
+ * empty, because an aliased repository serves its working tree whatever
123
+ * ref is asked for
124
+ * - "owner/repo[/path]/text": the files of the package at `path` (the
125
+ * repository root when omitted), keyed by file name: its pxt.json and
126
+ * every file that pxt.json lists
127
+ *
128
+ * Throws an error carrying the HTTP `statusCode` to answer with: 404 when
129
+ * the repository is not aliased, or when the package's pxt.json or a file it
130
+ * lists does not exist; 400 when the path lies outside the repository's
131
+ * directory, the pxt.json is not valid or lists a file outside the package's
132
+ * directory, or the request matches none of the forms above. Every error is
133
+ * also written to the console.
134
+ */
135
+ function resolveLocalRepoRequest(localRepos, request) {
136
+ try {
137
+ return answerLocalRepoRequest(localRepos, request);
138
+ }
139
+ catch (e) {
140
+ pxt.error(e.message);
141
+ throw e;
142
+ }
143
+ }
144
+ exports.resolveLocalRepoRequest = resolveLocalRepoRequest;
145
+ function answerLocalRepoRequest(localRepos, request) {
146
+ const elts = request.split("/").filter(e => !!e);
147
+ const repo = elts.length >= 2 ? pxt.github.parseRepoId(elts.slice(0, 2).join("/")) : undefined;
148
+ const root = repo && localRepos[repo.slug.toLowerCase()];
149
+ if (!root)
150
+ throw requestError(404, `${request}: not a locally served repository`);
151
+ const rest = elts.slice(2);
152
+ if (!rest.length) {
153
+ pxt.debug(`local repo: ${repo.slug} metadata from ${root}`);
154
+ const rootConfig = pxt.Package.parseAndValidConfig(readTextOpt(path.join(root, pxt.CONFIG_NAME)));
155
+ return {
156
+ name: repo.project,
157
+ description: (rootConfig === null || rootConfig === void 0 ? void 0 : rootConfig.description) || ""
158
+ };
159
+ }
160
+ if (rest.length == 1 && rest[0] == "refs") {
161
+ pxt.debug(`local repo: ${repo.slug} refs (working tree only)`);
162
+ return { refs: {} };
163
+ }
164
+ if (rest[rest.length - 1] == "text") {
165
+ const fileName = rest.slice(0, -1).join("/");
166
+ return readPackageFiles(pxt.github.join(repo.slug, fileName), root, fileName);
167
+ }
168
+ throw requestError(400, `${request}: unknown local repository request`);
169
+ }