@anvil-works/anvil-cli 0.7.0-canary.16 → 0.7.0-canary.18
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/README.md +9 -3
- package/dist/WatchSession.d.ts.map +1 -1
- package/dist/cli.js +399 -64
- package/dist/index.js +276 -32
- package/dist/program.d.ts.map +1 -1
- package/dist/services/auth.d.ts.map +1 -1
- package/dist/validatePython.d.ts +10 -0
- package/dist/validatePython.d.ts.map +1 -0
- package/dist/validators.d.ts +2 -1
- package/dist/validators.d.ts.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -170,17 +170,18 @@ Useful options:
|
|
|
170
170
|
|
|
171
171
|
### `anvil validate`
|
|
172
172
|
|
|
173
|
-
Check an Anvil app file before syncing, or when the CLI reports a validation error.
|
|
173
|
+
Check an Anvil app file or directory before syncing, or when the CLI reports a validation error.
|
|
174
174
|
|
|
175
175
|
```bash
|
|
176
|
-
anvil validate <
|
|
176
|
+
anvil validate <path>
|
|
177
177
|
```
|
|
178
178
|
|
|
179
|
-
|
|
179
|
+
When given a file, the command chooses the validator from the file path. When given a directory, it recursively validates supported files under that directory. It supports:
|
|
180
180
|
|
|
181
181
|
- `anvil.yaml`
|
|
182
182
|
- `.yaml` files under `client_code/`, including `client_code/**/form_template.yaml`
|
|
183
183
|
- `.html` files under `client_code/`, including `client_code/**/form_template.html` and module-form `client_code/<Form>.html`
|
|
184
|
+
- `.py` files under `client_code/` and `server_code/`
|
|
184
185
|
|
|
185
186
|
Common examples:
|
|
186
187
|
|
|
@@ -190,10 +191,15 @@ anvil validate client_code/Form1/form_template.yaml
|
|
|
190
191
|
anvil validate client_code/Form1.yaml
|
|
191
192
|
anvil validate client_code/Form1/form_template.html
|
|
192
193
|
anvil validate client_code/Form1.html
|
|
194
|
+
anvil validate client_code/Helpers.py
|
|
195
|
+
anvil validate server_code/ServerModule.py
|
|
196
|
+
anvil validate .
|
|
193
197
|
```
|
|
194
198
|
|
|
195
199
|
HTML validation parses the template with `@anvil-works/form-template-parser`, checks optional YAML frontmatter, and reports semantic issues such as duplicate explicit component names or invalid component types.
|
|
196
200
|
|
|
201
|
+
Python validation checks syntax using a local Python interpreter (`ANVIL_PYTHON`, `python3`, or `python`). Form Python under `client_code/` also checks for the matching designer-template import and form class inheritance when a sibling form template exists.
|
|
202
|
+
|
|
197
203
|
When the file is valid, the command prints a success message and exits with status `0`. When the file is invalid, it prints path-specific validation issues and exits with a non-zero status.
|
|
198
204
|
|
|
199
205
|
Use `--json` with `anvil validate` to emit structured NDJSON output for scripts or LLM tooling.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"WatchSession.d.ts","sourceRoot":"","sources":["../src/WatchSession.ts"],"names":[],"mappings":"AASA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EACH,uBAAuB,EACvB,mBAAmB,EAOtB,MAAM,eAAe,CAAC;
|
|
1
|
+
{"version":3,"file":"WatchSession.d.ts","sourceRoot":"","sources":["../src/WatchSession.ts"],"names":[],"mappings":"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"}
|
package/dist/cli.js
CHANGED
|
@@ -13895,6 +13895,7 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
13895
13895
|
function setRepoContext(repoPath) {
|
|
13896
13896
|
repoContext = repoPath;
|
|
13897
13897
|
}
|
|
13898
|
+
const inFlightRefreshes = new Map();
|
|
13898
13899
|
async function verifyAuth(authToken, anvilUrl = getDefaultAnvilUrl()) {
|
|
13899
13900
|
try {
|
|
13900
13901
|
const resp = await fetch(`${anvilUrl}/ide/api/_/user`, {
|
|
@@ -13948,53 +13949,91 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
13948
13949
|
}
|
|
13949
13950
|
async function auth_getValidAuthToken(anvilUrl = getDefaultAnvilUrl(), username) {
|
|
13950
13951
|
const normalized = normalizeAnvilUrl(anvilUrl);
|
|
13951
|
-
|
|
13952
|
-
|
|
13953
|
-
if (inMemoryAuth && authMatches(inMemoryAuth, normalized, username)) tokens = inMemoryAuth.tokens;
|
|
13954
|
-
else {
|
|
13955
|
-
repoAuth = repoContext ? await readAuthFromRepo(repoContext) : void 0;
|
|
13956
|
-
tokens = repoAuth && authMatches(repoAuth, normalized, username) ? repoAuth.tokens : getTokensAutoSelect(normalized, username);
|
|
13957
|
-
}
|
|
13952
|
+
const source = await resolveTokenSource(normalized, username);
|
|
13953
|
+
const tokens = getSourceTokens(source);
|
|
13958
13954
|
if (!tokens.authToken && !tokens.refreshToken) throw errors_createAuthError.required("Not logged in. Please log in first.");
|
|
13959
13955
|
const isExpired = null !== tokens.authTokenExpiresAt && tokens.authTokenExpiresAt <= Math.floor(Date.now() / 1000) + 60;
|
|
13960
|
-
if (tokens.refreshToken && isExpired)
|
|
13956
|
+
if (tokens.refreshToken && isExpired) {
|
|
13957
|
+
const refreshKey = getRefreshKey(normalized, username, source, tokens.refreshToken);
|
|
13958
|
+
const existingRefresh = inFlightRefreshes.get(refreshKey);
|
|
13959
|
+
if (existingRefresh) return existingRefresh;
|
|
13960
|
+
const refreshPromise = refreshAndStoreAccessToken(normalized, username, source, tokens.refreshToken).finally(()=>{
|
|
13961
|
+
inFlightRefreshes.delete(refreshKey);
|
|
13962
|
+
});
|
|
13963
|
+
inFlightRefreshes.set(refreshKey, refreshPromise);
|
|
13964
|
+
return refreshPromise;
|
|
13965
|
+
}
|
|
13966
|
+
if (tokens.authToken) return tokens.authToken;
|
|
13967
|
+
throw errors_createAuthError.required("No valid token available");
|
|
13968
|
+
}
|
|
13969
|
+
async function resolveTokenSource(normalizedUrl, username) {
|
|
13970
|
+
if (inMemoryAuth && authMatches(inMemoryAuth, normalizedUrl, username)) return {
|
|
13971
|
+
type: "memory",
|
|
13972
|
+
auth: inMemoryAuth
|
|
13973
|
+
};
|
|
13974
|
+
const repoAuth = repoContext ? await readAuthFromRepo(repoContext) : void 0;
|
|
13975
|
+
if (repoAuth && authMatches(repoAuth, normalizedUrl, username)) return {
|
|
13976
|
+
type: "repo",
|
|
13977
|
+
auth: repoAuth,
|
|
13978
|
+
repoPath: repoContext
|
|
13979
|
+
};
|
|
13980
|
+
const tokens = getTokensAutoSelect(normalizedUrl, username);
|
|
13981
|
+
return {
|
|
13982
|
+
type: "store",
|
|
13983
|
+
tokens,
|
|
13984
|
+
accountUsername: getStoreAccountUsername(normalizedUrl, username, tokens.refreshToken ?? void 0)
|
|
13985
|
+
};
|
|
13986
|
+
}
|
|
13987
|
+
function getSourceTokens(source) {
|
|
13988
|
+
return "store" === source.type ? source.tokens : source.auth.tokens;
|
|
13989
|
+
}
|
|
13990
|
+
function getStoreAccountUsername(normalizedUrl, username, refreshToken) {
|
|
13991
|
+
if (username) return username;
|
|
13992
|
+
const urlTokens = getUrlTokens(normalizedUrl);
|
|
13993
|
+
if (!urlTokens) return;
|
|
13994
|
+
const accounts = Object.keys(urlTokens);
|
|
13995
|
+
if (1 === accounts.length) return accounts[0];
|
|
13996
|
+
return refreshToken ? findAccountByRefreshToken(normalizedUrl, refreshToken) : void 0;
|
|
13997
|
+
}
|
|
13998
|
+
function getRefreshKey(normalizedUrl, username, source, refreshToken) {
|
|
13999
|
+
switch(source.type){
|
|
14000
|
+
case "memory":
|
|
14001
|
+
return `memory:${normalizedUrl}:${source.auth.username}`;
|
|
14002
|
+
case "repo":
|
|
14003
|
+
return `repo:${source.repoPath}:${normalizedUrl}:${source.auth.username}`;
|
|
14004
|
+
case "store":
|
|
14005
|
+
return `store:${normalizedUrl}:${source.accountUsername ?? username ?? refreshToken}`;
|
|
14006
|
+
}
|
|
14007
|
+
}
|
|
14008
|
+
async function refreshAndStoreAccessToken(normalizedUrl, username, source, refreshToken) {
|
|
14009
|
+
try {
|
|
14010
|
+
const tokens = getSourceTokens(source);
|
|
13961
14011
|
const clientId = tokens.clientId ?? ANVIL_SYNC_CLIENT_ID;
|
|
13962
|
-
const tokenData = await refreshAccessToken(
|
|
13963
|
-
const newExpiresAt = Math.floor(Date.now() / 1000) + tokenData.expires_in;
|
|
14012
|
+
const tokenData = await refreshAccessToken(refreshToken, normalizedUrl, clientId);
|
|
13964
14013
|
const newTokens = {
|
|
13965
14014
|
authToken: tokenData.access_token,
|
|
13966
14015
|
refreshToken: tokenData.refresh_token,
|
|
13967
|
-
authTokenExpiresAt:
|
|
14016
|
+
authTokenExpiresAt: Math.floor(Date.now() / 1000) + tokenData.expires_in,
|
|
13968
14017
|
clientId
|
|
13969
14018
|
};
|
|
13970
|
-
if (
|
|
13971
|
-
else if (
|
|
14019
|
+
if ("memory" === source.type) source.auth.tokens = newTokens;
|
|
14020
|
+
else if ("repo" === source.type) {
|
|
13972
14021
|
const encryptionKey = process.env["ANVIL_AUTH_FILE_ENCRYPTION_KEY"];
|
|
13973
14022
|
if (!encryptionKey) throw errors_createAuthError.required("Cannot refresh repo auth without ANVIL_AUTH_FILE_ENCRYPTION_KEY.");
|
|
13974
|
-
|
|
13975
|
-
await writeAuthToFile(
|
|
14023
|
+
source.auth.tokens = newTokens;
|
|
14024
|
+
await writeAuthToFile(source.repoPath, source.auth, encryptionKey);
|
|
13976
14025
|
} else {
|
|
13977
|
-
|
|
13978
|
-
|
|
13979
|
-
const urlTokens = getUrlTokens(normalized);
|
|
13980
|
-
if (urlTokens) {
|
|
13981
|
-
const accounts = Object.keys(urlTokens);
|
|
13982
|
-
accountUsername = 1 === accounts.length ? accounts[0] : findAccountByRefreshToken(normalized, tokens.refreshToken);
|
|
13983
|
-
}
|
|
13984
|
-
}
|
|
13985
|
-
if (!accountUsername) {
|
|
13986
|
-
await clearTokensForAccount(normalized, username, tokens.refreshToken);
|
|
14026
|
+
if (!source.accountUsername) {
|
|
14027
|
+
await clearTokensForAccount(normalizedUrl, username, refreshToken);
|
|
13987
14028
|
throw errors_createAuthError.required("Cannot determine which account to refresh. Please log in again.");
|
|
13988
14029
|
}
|
|
13989
|
-
setAccountTokens(
|
|
14030
|
+
setAccountTokens(normalizedUrl, source.accountUsername, newTokens);
|
|
13990
14031
|
}
|
|
13991
14032
|
return tokenData.access_token;
|
|
13992
14033
|
} catch (e) {
|
|
13993
|
-
await clearTokensForAccount(
|
|
14034
|
+
await clearTokensForAccount(normalizedUrl, username, refreshToken);
|
|
13994
14035
|
throw e;
|
|
13995
14036
|
}
|
|
13996
|
-
if (tokens.authToken) return tokens.authToken;
|
|
13997
|
-
throw errors_createAuthError.required("No valid token available");
|
|
13998
14037
|
}
|
|
13999
14038
|
async function clearTokensForAccount(url, username, refreshToken) {
|
|
14000
14039
|
if (inMemoryAuth || repoContext && await readAuthFromRepo(repoContext)) return;
|
|
@@ -16249,6 +16288,167 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
16249
16288
|
ok: true
|
|
16250
16289
|
};
|
|
16251
16290
|
}
|
|
16291
|
+
const PYTHON_VALIDATOR_SCRIPT = String.raw`
|
|
16292
|
+
import ast
|
|
16293
|
+
import json
|
|
16294
|
+
import sys
|
|
16295
|
+
|
|
16296
|
+
|
|
16297
|
+
def issue_path(error):
|
|
16298
|
+
if getattr(error, "lineno", None) is None:
|
|
16299
|
+
return "root"
|
|
16300
|
+
if getattr(error, "offset", None) is None:
|
|
16301
|
+
return f"line {error.lineno}"
|
|
16302
|
+
return f"line {error.lineno}, column {error.offset}"
|
|
16303
|
+
|
|
16304
|
+
|
|
16305
|
+
def base_name(node):
|
|
16306
|
+
if isinstance(node, ast.Name):
|
|
16307
|
+
return node.id
|
|
16308
|
+
if isinstance(node, ast.Attribute):
|
|
16309
|
+
return node.attr
|
|
16310
|
+
return None
|
|
16311
|
+
|
|
16312
|
+
|
|
16313
|
+
def imports_template_from_anvil_designer(node, template_name):
|
|
16314
|
+
if node.level < 1:
|
|
16315
|
+
return False
|
|
16316
|
+
if node.module not in ("_anvil_designer", None):
|
|
16317
|
+
return False
|
|
16318
|
+
return any(alias.name == template_name for alias in node.names)
|
|
16319
|
+
|
|
16320
|
+
|
|
16321
|
+
file_path = sys.argv[1]
|
|
16322
|
+
class_name = sys.argv[2]
|
|
16323
|
+
template_name = f"{class_name}Template" if class_name else ""
|
|
16324
|
+
source = sys.stdin.read()
|
|
16325
|
+
issues = []
|
|
16326
|
+
|
|
16327
|
+
try:
|
|
16328
|
+
compile(source, file_path, "exec")
|
|
16329
|
+
except SyntaxError as error:
|
|
16330
|
+
issues.append({"path": issue_path(error), "message": error.msg})
|
|
16331
|
+
print(json.dumps({"issues": issues}))
|
|
16332
|
+
sys.exit(0)
|
|
16333
|
+
|
|
16334
|
+
if class_name:
|
|
16335
|
+
tree = ast.parse(source, filename=file_path)
|
|
16336
|
+
has_template_import = False
|
|
16337
|
+
has_form_class = False
|
|
16338
|
+
|
|
16339
|
+
for node in ast.walk(tree):
|
|
16340
|
+
if isinstance(node, ast.ImportFrom) and imports_template_from_anvil_designer(node, template_name):
|
|
16341
|
+
has_template_import = True
|
|
16342
|
+
elif isinstance(node, ast.ClassDef) and node.name == class_name:
|
|
16343
|
+
if any(base_name(base) == template_name for base in node.bases):
|
|
16344
|
+
has_form_class = True
|
|
16345
|
+
|
|
16346
|
+
if not has_template_import:
|
|
16347
|
+
issues.append({
|
|
16348
|
+
"path": "root",
|
|
16349
|
+
"message": f"missing {chr(96)}from ._anvil_designer import {template_name}{chr(96)}",
|
|
16350
|
+
})
|
|
16351
|
+
if not has_form_class:
|
|
16352
|
+
issues.append({
|
|
16353
|
+
"path": "root",
|
|
16354
|
+
"message": f"missing class {chr(96)}{class_name}{chr(96)} inheriting from {chr(96)}{template_name}{chr(96)}",
|
|
16355
|
+
})
|
|
16356
|
+
|
|
16357
|
+
print(json.dumps({"issues": issues}))
|
|
16358
|
+
`;
|
|
16359
|
+
function findPythonInterpreter() {
|
|
16360
|
+
const candidates = [
|
|
16361
|
+
process.env.ANVIL_PYTHON,
|
|
16362
|
+
"python3",
|
|
16363
|
+
"python"
|
|
16364
|
+
].filter((candidate)=>"string" == typeof candidate && candidate.length > 0);
|
|
16365
|
+
for (const candidate of candidates){
|
|
16366
|
+
const result = (0, external_child_process_namespaceObject.spawnSync)(candidate, [
|
|
16367
|
+
"--version"
|
|
16368
|
+
], {
|
|
16369
|
+
encoding: "utf8"
|
|
16370
|
+
});
|
|
16371
|
+
if (!result.error && 0 === result.status) return candidate;
|
|
16372
|
+
}
|
|
16373
|
+
return null;
|
|
16374
|
+
}
|
|
16375
|
+
function isClientFormPython(filePath) {
|
|
16376
|
+
if ("__init__.py" === external_path_default().basename(filePath)) {
|
|
16377
|
+
const formDir = external_path_default().dirname(filePath);
|
|
16378
|
+
return external_fs_.existsSync(external_path_default().join(formDir, "form_template.html")) || external_fs_.existsSync(external_path_default().join(formDir, "form_template.yaml"));
|
|
16379
|
+
}
|
|
16380
|
+
const parsed = external_path_default().parse(filePath);
|
|
16381
|
+
return external_fs_.existsSync(external_path_default().join(parsed.dir, `${parsed.name}.html`)) || external_fs_.existsSync(external_path_default().join(parsed.dir, `${parsed.name}.yaml`));
|
|
16382
|
+
}
|
|
16383
|
+
function expectedFormClassName(filePath) {
|
|
16384
|
+
if (!isClientFormPython(filePath)) return null;
|
|
16385
|
+
if ("__init__.py" === external_path_default().basename(filePath)) return external_path_default().basename(external_path_default().dirname(filePath));
|
|
16386
|
+
return external_path_default().basename(filePath, ".py");
|
|
16387
|
+
}
|
|
16388
|
+
function isUnderDirectory(filePath, directoryPath) {
|
|
16389
|
+
const relativePath = external_path_default().relative(directoryPath, filePath);
|
|
16390
|
+
return relativePath.length > 0 && !relativePath.startsWith("..") && !external_path_default().isAbsolute(relativePath);
|
|
16391
|
+
}
|
|
16392
|
+
function validatePython(filePath, fileContent, appRoot) {
|
|
16393
|
+
const interpreter = findPythonInterpreter();
|
|
16394
|
+
if (!interpreter) return {
|
|
16395
|
+
ok: false,
|
|
16396
|
+
message: "Python file could not be validated",
|
|
16397
|
+
issues: [
|
|
16398
|
+
{
|
|
16399
|
+
path: "root",
|
|
16400
|
+
message: "Python validation requires python3 or python on PATH, or ANVIL_PYTHON to be set"
|
|
16401
|
+
}
|
|
16402
|
+
]
|
|
16403
|
+
};
|
|
16404
|
+
const absolutePath = external_path_default().resolve(filePath);
|
|
16405
|
+
const clientCodeRoot = appRoot ? external_path_default().join(appRoot, "client_code") : null;
|
|
16406
|
+
const formClassName = clientCodeRoot && isUnderDirectory(absolutePath, clientCodeRoot) ? expectedFormClassName(absolutePath) : null;
|
|
16407
|
+
const result = (0, external_child_process_namespaceObject.spawnSync)(interpreter, [
|
|
16408
|
+
"-c",
|
|
16409
|
+
PYTHON_VALIDATOR_SCRIPT,
|
|
16410
|
+
filePath,
|
|
16411
|
+
formClassName ?? ""
|
|
16412
|
+
], {
|
|
16413
|
+
encoding: "utf8",
|
|
16414
|
+
input: fileContent,
|
|
16415
|
+
maxBuffer: 1048576
|
|
16416
|
+
});
|
|
16417
|
+
if (result.error || 0 !== result.status) return {
|
|
16418
|
+
ok: false,
|
|
16419
|
+
message: "Python file could not be validated",
|
|
16420
|
+
issues: [
|
|
16421
|
+
{
|
|
16422
|
+
path: "root",
|
|
16423
|
+
message: result.error?.message ?? (result.stderr.trim() || "Python validator failed")
|
|
16424
|
+
}
|
|
16425
|
+
]
|
|
16426
|
+
};
|
|
16427
|
+
let parsed;
|
|
16428
|
+
try {
|
|
16429
|
+
parsed = JSON.parse(result.stdout);
|
|
16430
|
+
} catch (error) {
|
|
16431
|
+
return {
|
|
16432
|
+
ok: false,
|
|
16433
|
+
message: "Python file could not be validated",
|
|
16434
|
+
issues: [
|
|
16435
|
+
{
|
|
16436
|
+
path: "root",
|
|
16437
|
+
message: error instanceof Error ? error.message : String(error)
|
|
16438
|
+
}
|
|
16439
|
+
]
|
|
16440
|
+
};
|
|
16441
|
+
}
|
|
16442
|
+
const issues = parsed.issues ?? [];
|
|
16443
|
+
if (issues.length > 0) return {
|
|
16444
|
+
ok: false,
|
|
16445
|
+
message: "Python file is invalid for Anvil",
|
|
16446
|
+
issues
|
|
16447
|
+
};
|
|
16448
|
+
return {
|
|
16449
|
+
ok: true
|
|
16450
|
+
};
|
|
16451
|
+
}
|
|
16252
16452
|
const validators_IDENTIFIER_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
16253
16453
|
const DB_ACCESS_LEVELS = new Set([
|
|
16254
16454
|
"none",
|
|
@@ -16646,6 +16846,13 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
16646
16846
|
const relativeToClientCode = normalizedPath.slice(startIndex + clientCodePrefix.length);
|
|
16647
16847
|
if (relativeToClientCode.endsWith(".yaml")) return "form_template.yaml";
|
|
16648
16848
|
if (relativeToClientCode.endsWith(".html")) return "form_template.html";
|
|
16849
|
+
if (relativeToClientCode.endsWith(".py")) return "python";
|
|
16850
|
+
}
|
|
16851
|
+
const serverCodePrefix = "server_code/";
|
|
16852
|
+
if (normalizedPath.startsWith(serverCodePrefix) || normalizedPath.includes(`/${serverCodePrefix}`)) {
|
|
16853
|
+
const startIndex = normalizedPath.lastIndexOf(serverCodePrefix);
|
|
16854
|
+
const relativeToServerCode = normalizedPath.slice(startIndex + serverCodePrefix.length);
|
|
16855
|
+
if (relativeToServerCode.endsWith(".py")) return "python";
|
|
16649
16856
|
}
|
|
16650
16857
|
return null;
|
|
16651
16858
|
}
|
|
@@ -16753,6 +16960,35 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
16753
16960
|
target
|
|
16754
16961
|
};
|
|
16755
16962
|
}
|
|
16963
|
+
if ("python" === target) {
|
|
16964
|
+
if (existingPath && appRoot) {
|
|
16965
|
+
const clientCodeRoot = external_path_default().join(appRoot, "client_code");
|
|
16966
|
+
const serverCodeRoot = external_path_default().join(appRoot, "server_code");
|
|
16967
|
+
const relativeToClientCode = external_path_default().relative(clientCodeRoot, existingPath);
|
|
16968
|
+
const relativeToServerCode = external_path_default().relative(serverCodeRoot, existingPath);
|
|
16969
|
+
const isInsideClientCode = relativeToClientCode.length > 0 && !relativeToClientCode.startsWith("..") && !external_path_default().isAbsolute(relativeToClientCode);
|
|
16970
|
+
const isInsideServerCode = relativeToServerCode.length > 0 && !relativeToServerCode.startsWith("..") && !external_path_default().isAbsolute(relativeToServerCode);
|
|
16971
|
+
if (!isInsideClientCode && !isInsideServerCode || !existingPath.endsWith(".py")) return {
|
|
16972
|
+
ok: false,
|
|
16973
|
+
target,
|
|
16974
|
+
message: "File is not an Anvil app Python file",
|
|
16975
|
+
issues: [
|
|
16976
|
+
{
|
|
16977
|
+
path: "",
|
|
16978
|
+
message: `expected a .py file under ${clientCodeRoot} or ${serverCodeRoot}`
|
|
16979
|
+
}
|
|
16980
|
+
]
|
|
16981
|
+
};
|
|
16982
|
+
}
|
|
16983
|
+
const result = validatePython(filePath, fileContent, appRoot);
|
|
16984
|
+
return result.ok ? {
|
|
16985
|
+
ok: true,
|
|
16986
|
+
target
|
|
16987
|
+
} : {
|
|
16988
|
+
...result,
|
|
16989
|
+
target
|
|
16990
|
+
};
|
|
16991
|
+
}
|
|
16756
16992
|
return {
|
|
16757
16993
|
ok: false,
|
|
16758
16994
|
target: "unknown",
|
|
@@ -16760,7 +16996,7 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
16760
16996
|
issues: [
|
|
16761
16997
|
{
|
|
16762
16998
|
path: "",
|
|
16763
|
-
message: "supported paths are anvil.yaml, client_code/**/form_template.yaml, client_code/**/*.yaml,
|
|
16999
|
+
message: "supported paths are anvil.yaml, client_code/**/form_template.yaml, client_code/**/*.yaml, client_code/**/*.html, client_code/**/*.py, and server_code/**/*.py"
|
|
16764
17000
|
}
|
|
16765
17001
|
]
|
|
16766
17002
|
};
|
|
@@ -17845,6 +18081,12 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
17845
18081
|
}
|
|
17846
18082
|
}
|
|
17847
18083
|
function authDiagnostic(error) {
|
|
18084
|
+
if ("token_refresh_failed" === error.type && error.message?.includes("invalid_grant")) return {
|
|
18085
|
+
code: error.type,
|
|
18086
|
+
recoverability: "requires_user_action",
|
|
18087
|
+
requiresUserAction: true,
|
|
18088
|
+
suggestedAction: "Your Anvil session auth has expired or was invalidated. Run anvil login or restart the agent/watch session, then retry."
|
|
18089
|
+
};
|
|
17848
18090
|
return {
|
|
17849
18091
|
code: error.type,
|
|
17850
18092
|
recoverability: "requires_user_action",
|
|
@@ -18761,7 +19003,7 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
18761
19003
|
getAuthToken: ()=>this.authToken,
|
|
18762
19004
|
setAuthToken: (token)=>{
|
|
18763
19005
|
this.authToken = token;
|
|
18764
|
-
this.wsClient?.updateAuthToken(token);
|
|
19006
|
+
this.wsClient?.updateAuthToken?.(token);
|
|
18765
19007
|
},
|
|
18766
19008
|
getCurrentBranch: ()=>this.currentBranch,
|
|
18767
19009
|
getCommitId: ()=>this.commitId,
|
|
@@ -18781,7 +19023,7 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
18781
19023
|
getAuthToken: ()=>this.authToken,
|
|
18782
19024
|
setAuthToken: (token)=>{
|
|
18783
19025
|
this.authToken = token;
|
|
18784
|
-
this.wsClient?.updateAuthToken(token);
|
|
19026
|
+
this.wsClient?.updateAuthToken?.(token);
|
|
18785
19027
|
},
|
|
18786
19028
|
getCurrentBranch: ()=>this.currentBranch,
|
|
18787
19029
|
getCommitId: ()=>this.commitId,
|
|
@@ -18949,6 +19191,8 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
18949
19191
|
]);
|
|
18950
19192
|
}
|
|
18951
19193
|
logger_logger.verbose(external_chalk_default().cyan(`Environment branch ${"snapshot" === source ? "snapshot" : "update"}: `) + external_chalk_default().bold(`${this.currentBranch} -> ${targetBranch}`));
|
|
19194
|
+
this.authToken = await auth_getValidAuthToken(this.anvilUrl, this.username);
|
|
19195
|
+
this.wsClient?.updateAuthToken?.(this.authToken);
|
|
18952
19196
|
const fetchUrl = getGitFetchUrl(this.appId, this.authToken, this.anvilUrl);
|
|
18953
19197
|
const envRemoteRef = environmentRemoteRefForBranch(targetBranch);
|
|
18954
19198
|
await this.gitService.fetch(fetchUrl, environmentFetchRefSpecForBranch(targetBranch));
|
|
@@ -22874,6 +23118,123 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
22874
23118
|
}
|
|
22875
23119
|
}).catch(()=>{});
|
|
22876
23120
|
}
|
|
23121
|
+
function collectValidationFiles(dir) {
|
|
23122
|
+
const files = [];
|
|
23123
|
+
const ignoredDirectories = new Set([
|
|
23124
|
+
".anvil",
|
|
23125
|
+
".git",
|
|
23126
|
+
"node_modules",
|
|
23127
|
+
"__pycache__"
|
|
23128
|
+
]);
|
|
23129
|
+
function walk(currentDir) {
|
|
23130
|
+
for (const entry of (0, external_fs_.readdirSync)(currentDir, {
|
|
23131
|
+
withFileTypes: true
|
|
23132
|
+
})){
|
|
23133
|
+
const entryPath = (0, external_path_namespaceObject.join)(currentDir, entry.name);
|
|
23134
|
+
if (entry.isDirectory()) {
|
|
23135
|
+
if (!ignoredDirectories.has(entry.name)) walk(entryPath);
|
|
23136
|
+
continue;
|
|
23137
|
+
}
|
|
23138
|
+
if (entry.isFile() && inferValidationTarget(entryPath)) files.push(entryPath);
|
|
23139
|
+
}
|
|
23140
|
+
}
|
|
23141
|
+
walk(dir);
|
|
23142
|
+
return files.sort();
|
|
23143
|
+
}
|
|
23144
|
+
function validateSingleFile(file) {
|
|
23145
|
+
let fileContent;
|
|
23146
|
+
try {
|
|
23147
|
+
fileContent = (0, external_fs_.readFileSync)(file, "utf8");
|
|
23148
|
+
} catch (error) {
|
|
23149
|
+
return {
|
|
23150
|
+
file,
|
|
23151
|
+
type: "unknown",
|
|
23152
|
+
valid: false,
|
|
23153
|
+
error: `Failed to read ${file}: ${error.message}`,
|
|
23154
|
+
issues: [
|
|
23155
|
+
{
|
|
23156
|
+
path: "",
|
|
23157
|
+
message: error.message
|
|
23158
|
+
}
|
|
23159
|
+
]
|
|
23160
|
+
};
|
|
23161
|
+
}
|
|
23162
|
+
const result = validatePath(file, fileContent);
|
|
23163
|
+
if (result.ok) return {
|
|
23164
|
+
file,
|
|
23165
|
+
type: result.target,
|
|
23166
|
+
valid: true
|
|
23167
|
+
};
|
|
23168
|
+
return {
|
|
23169
|
+
file,
|
|
23170
|
+
type: result.target,
|
|
23171
|
+
valid: false,
|
|
23172
|
+
error: result.message,
|
|
23173
|
+
issues: result.issues
|
|
23174
|
+
};
|
|
23175
|
+
}
|
|
23176
|
+
function logValidationFailure(fileResult) {
|
|
23177
|
+
logger_logger.error(`${fileResult.file} failed validation: ${fileResult.error ?? "Validation failed"}`);
|
|
23178
|
+
for (const issue of fileResult.issues ?? []){
|
|
23179
|
+
const issuePath = issue.path.length > 0 ? issue.path : "root";
|
|
23180
|
+
logger_logger.error(` - ${issuePath}: ${issue.message}`);
|
|
23181
|
+
}
|
|
23182
|
+
}
|
|
23183
|
+
function handleValidateFile(file) {
|
|
23184
|
+
const fileResult = validateSingleFile(file);
|
|
23185
|
+
if (fileResult.valid) {
|
|
23186
|
+
if (getGlobalOutputConfig().jsonMode) logJsonResult(true, {
|
|
23187
|
+
data: {
|
|
23188
|
+
file: fileResult.file,
|
|
23189
|
+
type: fileResult.type,
|
|
23190
|
+
valid: true
|
|
23191
|
+
}
|
|
23192
|
+
});
|
|
23193
|
+
else logger_logger.success(`${fileResult.file} is valid (${fileResult.type})`);
|
|
23194
|
+
return;
|
|
23195
|
+
}
|
|
23196
|
+
if (getGlobalOutputConfig().jsonMode) logJsonResult(false, {
|
|
23197
|
+
error: fileResult.error,
|
|
23198
|
+
data: {
|
|
23199
|
+
file: fileResult.file,
|
|
23200
|
+
type: fileResult.type,
|
|
23201
|
+
issues: fileResult.issues ?? []
|
|
23202
|
+
}
|
|
23203
|
+
});
|
|
23204
|
+
else logValidationFailure(fileResult);
|
|
23205
|
+
process.exitCode = 1;
|
|
23206
|
+
}
|
|
23207
|
+
function handleValidateDirectory(dir) {
|
|
23208
|
+
const files = collectValidationFiles(dir);
|
|
23209
|
+
if (0 === files.length) {
|
|
23210
|
+
const error = `No supported files found under ${dir}`;
|
|
23211
|
+
if (getGlobalOutputConfig().jsonMode) logJsonResult(false, {
|
|
23212
|
+
error,
|
|
23213
|
+
data: {
|
|
23214
|
+
file: dir,
|
|
23215
|
+
valid: false,
|
|
23216
|
+
files: []
|
|
23217
|
+
}
|
|
23218
|
+
});
|
|
23219
|
+
else logger_logger.error(error);
|
|
23220
|
+
process.exitCode = 1;
|
|
23221
|
+
return;
|
|
23222
|
+
}
|
|
23223
|
+
const fileResults = files.map(validateSingleFile);
|
|
23224
|
+
const failedResults = fileResults.filter((result)=>!result.valid);
|
|
23225
|
+
if (getGlobalOutputConfig().jsonMode) logJsonResult(0 === failedResults.length, {
|
|
23226
|
+
error: failedResults.length > 0 ? "Validation failed" : void 0,
|
|
23227
|
+
data: {
|
|
23228
|
+
file: dir,
|
|
23229
|
+
valid: 0 === failedResults.length,
|
|
23230
|
+
count: fileResults.length,
|
|
23231
|
+
files: fileResults
|
|
23232
|
+
}
|
|
23233
|
+
});
|
|
23234
|
+
else for (const fileResult of fileResults)if (fileResult.valid) logger_logger.success(`${fileResult.file} is valid (${fileResult.type})`);
|
|
23235
|
+
else logValidationFailure(fileResult);
|
|
23236
|
+
if (failedResults.length > 0) process.exitCode = 1;
|
|
23237
|
+
}
|
|
22877
23238
|
function getUpdateInstructions(platform) {
|
|
22878
23239
|
if ("win32" === platform) return [
|
|
22879
23240
|
"try { irm https://anvil.works/install-cli.ps1 | iex } catch { npm install -g @anvil-works/anvil-cli@latest }",
|
|
@@ -22987,43 +23348,17 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
22987
23348
|
program.command("update").description("Update anvil to the latest version").alias("u").action(async ()=>{
|
|
22988
23349
|
await handleUpdateCommand();
|
|
22989
23350
|
});
|
|
22990
|
-
program.command("validate").description("Validate
|
|
22991
|
-
let
|
|
23351
|
+
program.command("validate").description("Validate Anvil app files by path or directory (anvil.yaml, client_code/**/*.yaml, client_code/**/*.html, client_code/**/*.py, or server_code/**/*.py)").argument("<path>", "Path to file or directory").action((file)=>{
|
|
23352
|
+
let pathStat;
|
|
22992
23353
|
try {
|
|
22993
|
-
|
|
23354
|
+
pathStat = (0, external_fs_.statSync)(file);
|
|
22994
23355
|
} catch (error) {
|
|
22995
|
-
logger_logger.error(`Failed to
|
|
23356
|
+
logger_logger.error(`Failed to access ${file}: ${error.message}`);
|
|
22996
23357
|
process.exitCode = 1;
|
|
22997
23358
|
return;
|
|
22998
23359
|
}
|
|
22999
|
-
|
|
23000
|
-
|
|
23001
|
-
if (getGlobalOutputConfig().jsonMode) logJsonResult(true, {
|
|
23002
|
-
data: {
|
|
23003
|
-
file,
|
|
23004
|
-
type: result.target,
|
|
23005
|
-
valid: true
|
|
23006
|
-
}
|
|
23007
|
-
});
|
|
23008
|
-
else logger_logger.success(`${file} is valid (${result.target})`);
|
|
23009
|
-
return;
|
|
23010
|
-
}
|
|
23011
|
-
if (getGlobalOutputConfig().jsonMode) logJsonResult(false, {
|
|
23012
|
-
error: result.message,
|
|
23013
|
-
data: {
|
|
23014
|
-
file,
|
|
23015
|
-
type: result.target,
|
|
23016
|
-
issues: result.issues
|
|
23017
|
-
}
|
|
23018
|
-
});
|
|
23019
|
-
else {
|
|
23020
|
-
logger_logger.error(`${file} failed validation: ${result.message}`);
|
|
23021
|
-
for (const issue of result.issues){
|
|
23022
|
-
const issuePath = issue.path.length > 0 ? issue.path : "root";
|
|
23023
|
-
logger_logger.error(` - ${issuePath}: ${issue.message}`);
|
|
23024
|
-
}
|
|
23025
|
-
}
|
|
23026
|
-
process.exitCode = 1;
|
|
23360
|
+
if (pathStat.isDirectory()) return void handleValidateDirectory(file);
|
|
23361
|
+
handleValidateFile(file);
|
|
23027
23362
|
});
|
|
23028
23363
|
if (watchCommand) {
|
|
23029
23364
|
const watchOptions = watchCommand.options.map((opt)=>{
|
package/dist/index.js
CHANGED
|
@@ -13885,6 +13885,7 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
13885
13885
|
function auth_setRepoContext(repoPath) {
|
|
13886
13886
|
repoContext = repoPath;
|
|
13887
13887
|
}
|
|
13888
|
+
const inFlightRefreshes = new Map();
|
|
13888
13889
|
async function auth_verifyAuth(authToken, anvilUrl = getDefaultAnvilUrl()) {
|
|
13889
13890
|
try {
|
|
13890
13891
|
const resp = await fetch(`${anvilUrl}/ide/api/_/user`, {
|
|
@@ -13938,53 +13939,91 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
13938
13939
|
}
|
|
13939
13940
|
async function auth_getValidAuthToken(anvilUrl = getDefaultAnvilUrl(), username) {
|
|
13940
13941
|
const normalized = config_normalizeAnvilUrl(anvilUrl);
|
|
13941
|
-
|
|
13942
|
-
|
|
13943
|
-
if (inMemoryAuth && authMatches(inMemoryAuth, normalized, username)) tokens = inMemoryAuth.tokens;
|
|
13944
|
-
else {
|
|
13945
|
-
repoAuth = repoContext ? await readAuthFromRepo(repoContext) : void 0;
|
|
13946
|
-
tokens = repoAuth && authMatches(repoAuth, normalized, username) ? repoAuth.tokens : getTokensAutoSelect(normalized, username);
|
|
13947
|
-
}
|
|
13942
|
+
const source = await resolveTokenSource(normalized, username);
|
|
13943
|
+
const tokens = getSourceTokens(source);
|
|
13948
13944
|
if (!tokens.authToken && !tokens.refreshToken) throw createAuthError.required("Not logged in. Please log in first.");
|
|
13949
13945
|
const isExpired = null !== tokens.authTokenExpiresAt && tokens.authTokenExpiresAt <= Math.floor(Date.now() / 1000) + 60;
|
|
13950
|
-
if (tokens.refreshToken && isExpired)
|
|
13946
|
+
if (tokens.refreshToken && isExpired) {
|
|
13947
|
+
const refreshKey = getRefreshKey(normalized, username, source, tokens.refreshToken);
|
|
13948
|
+
const existingRefresh = inFlightRefreshes.get(refreshKey);
|
|
13949
|
+
if (existingRefresh) return existingRefresh;
|
|
13950
|
+
const refreshPromise = refreshAndStoreAccessToken(normalized, username, source, tokens.refreshToken).finally(()=>{
|
|
13951
|
+
inFlightRefreshes.delete(refreshKey);
|
|
13952
|
+
});
|
|
13953
|
+
inFlightRefreshes.set(refreshKey, refreshPromise);
|
|
13954
|
+
return refreshPromise;
|
|
13955
|
+
}
|
|
13956
|
+
if (tokens.authToken) return tokens.authToken;
|
|
13957
|
+
throw createAuthError.required("No valid token available");
|
|
13958
|
+
}
|
|
13959
|
+
async function resolveTokenSource(normalizedUrl, username) {
|
|
13960
|
+
if (inMemoryAuth && authMatches(inMemoryAuth, normalizedUrl, username)) return {
|
|
13961
|
+
type: "memory",
|
|
13962
|
+
auth: inMemoryAuth
|
|
13963
|
+
};
|
|
13964
|
+
const repoAuth = repoContext ? await readAuthFromRepo(repoContext) : void 0;
|
|
13965
|
+
if (repoAuth && authMatches(repoAuth, normalizedUrl, username)) return {
|
|
13966
|
+
type: "repo",
|
|
13967
|
+
auth: repoAuth,
|
|
13968
|
+
repoPath: repoContext
|
|
13969
|
+
};
|
|
13970
|
+
const tokens = getTokensAutoSelect(normalizedUrl, username);
|
|
13971
|
+
return {
|
|
13972
|
+
type: "store",
|
|
13973
|
+
tokens,
|
|
13974
|
+
accountUsername: getStoreAccountUsername(normalizedUrl, username, tokens.refreshToken ?? void 0)
|
|
13975
|
+
};
|
|
13976
|
+
}
|
|
13977
|
+
function getSourceTokens(source) {
|
|
13978
|
+
return "store" === source.type ? source.tokens : source.auth.tokens;
|
|
13979
|
+
}
|
|
13980
|
+
function getStoreAccountUsername(normalizedUrl, username, refreshToken) {
|
|
13981
|
+
if (username) return username;
|
|
13982
|
+
const urlTokens = getUrlTokens(normalizedUrl);
|
|
13983
|
+
if (!urlTokens) return;
|
|
13984
|
+
const accounts = Object.keys(urlTokens);
|
|
13985
|
+
if (1 === accounts.length) return accounts[0];
|
|
13986
|
+
return refreshToken ? findAccountByRefreshToken(normalizedUrl, refreshToken) : void 0;
|
|
13987
|
+
}
|
|
13988
|
+
function getRefreshKey(normalizedUrl, username, source, refreshToken) {
|
|
13989
|
+
switch(source.type){
|
|
13990
|
+
case "memory":
|
|
13991
|
+
return `memory:${normalizedUrl}:${source.auth.username}`;
|
|
13992
|
+
case "repo":
|
|
13993
|
+
return `repo:${source.repoPath}:${normalizedUrl}:${source.auth.username}`;
|
|
13994
|
+
case "store":
|
|
13995
|
+
return `store:${normalizedUrl}:${source.accountUsername ?? username ?? refreshToken}`;
|
|
13996
|
+
}
|
|
13997
|
+
}
|
|
13998
|
+
async function refreshAndStoreAccessToken(normalizedUrl, username, source, refreshToken) {
|
|
13999
|
+
try {
|
|
14000
|
+
const tokens = getSourceTokens(source);
|
|
13951
14001
|
const clientId = tokens.clientId ?? ANVIL_SYNC_CLIENT_ID;
|
|
13952
|
-
const tokenData = await refreshAccessToken(
|
|
13953
|
-
const newExpiresAt = Math.floor(Date.now() / 1000) + tokenData.expires_in;
|
|
14002
|
+
const tokenData = await refreshAccessToken(refreshToken, normalizedUrl, clientId);
|
|
13954
14003
|
const newTokens = {
|
|
13955
14004
|
authToken: tokenData.access_token,
|
|
13956
14005
|
refreshToken: tokenData.refresh_token,
|
|
13957
|
-
authTokenExpiresAt:
|
|
14006
|
+
authTokenExpiresAt: Math.floor(Date.now() / 1000) + tokenData.expires_in,
|
|
13958
14007
|
clientId
|
|
13959
14008
|
};
|
|
13960
|
-
if (
|
|
13961
|
-
else if (
|
|
14009
|
+
if ("memory" === source.type) source.auth.tokens = newTokens;
|
|
14010
|
+
else if ("repo" === source.type) {
|
|
13962
14011
|
const encryptionKey = process.env["ANVIL_AUTH_FILE_ENCRYPTION_KEY"];
|
|
13963
14012
|
if (!encryptionKey) throw createAuthError.required("Cannot refresh repo auth without ANVIL_AUTH_FILE_ENCRYPTION_KEY.");
|
|
13964
|
-
|
|
13965
|
-
await writeAuthToFile(
|
|
14013
|
+
source.auth.tokens = newTokens;
|
|
14014
|
+
await writeAuthToFile(source.repoPath, source.auth, encryptionKey);
|
|
13966
14015
|
} else {
|
|
13967
|
-
|
|
13968
|
-
|
|
13969
|
-
const urlTokens = getUrlTokens(normalized);
|
|
13970
|
-
if (urlTokens) {
|
|
13971
|
-
const accounts = Object.keys(urlTokens);
|
|
13972
|
-
accountUsername = 1 === accounts.length ? accounts[0] : findAccountByRefreshToken(normalized, tokens.refreshToken);
|
|
13973
|
-
}
|
|
13974
|
-
}
|
|
13975
|
-
if (!accountUsername) {
|
|
13976
|
-
await clearTokensForAccount(normalized, username, tokens.refreshToken);
|
|
14016
|
+
if (!source.accountUsername) {
|
|
14017
|
+
await clearTokensForAccount(normalizedUrl, username, refreshToken);
|
|
13977
14018
|
throw createAuthError.required("Cannot determine which account to refresh. Please log in again.");
|
|
13978
14019
|
}
|
|
13979
|
-
setAccountTokens(
|
|
14020
|
+
setAccountTokens(normalizedUrl, source.accountUsername, newTokens);
|
|
13980
14021
|
}
|
|
13981
14022
|
return tokenData.access_token;
|
|
13982
14023
|
} catch (e) {
|
|
13983
|
-
await clearTokensForAccount(
|
|
14024
|
+
await clearTokensForAccount(normalizedUrl, username, refreshToken);
|
|
13984
14025
|
throw e;
|
|
13985
14026
|
}
|
|
13986
|
-
if (tokens.authToken) return tokens.authToken;
|
|
13987
|
-
throw createAuthError.required("No valid token available");
|
|
13988
14027
|
}
|
|
13989
14028
|
async function clearTokensForAccount(url, username, refreshToken) {
|
|
13990
14029
|
if (inMemoryAuth || repoContext && await readAuthFromRepo(repoContext)) return;
|
|
@@ -16278,6 +16317,167 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
16278
16317
|
ok: true
|
|
16279
16318
|
};
|
|
16280
16319
|
}
|
|
16320
|
+
const PYTHON_VALIDATOR_SCRIPT = String.raw`
|
|
16321
|
+
import ast
|
|
16322
|
+
import json
|
|
16323
|
+
import sys
|
|
16324
|
+
|
|
16325
|
+
|
|
16326
|
+
def issue_path(error):
|
|
16327
|
+
if getattr(error, "lineno", None) is None:
|
|
16328
|
+
return "root"
|
|
16329
|
+
if getattr(error, "offset", None) is None:
|
|
16330
|
+
return f"line {error.lineno}"
|
|
16331
|
+
return f"line {error.lineno}, column {error.offset}"
|
|
16332
|
+
|
|
16333
|
+
|
|
16334
|
+
def base_name(node):
|
|
16335
|
+
if isinstance(node, ast.Name):
|
|
16336
|
+
return node.id
|
|
16337
|
+
if isinstance(node, ast.Attribute):
|
|
16338
|
+
return node.attr
|
|
16339
|
+
return None
|
|
16340
|
+
|
|
16341
|
+
|
|
16342
|
+
def imports_template_from_anvil_designer(node, template_name):
|
|
16343
|
+
if node.level < 1:
|
|
16344
|
+
return False
|
|
16345
|
+
if node.module not in ("_anvil_designer", None):
|
|
16346
|
+
return False
|
|
16347
|
+
return any(alias.name == template_name for alias in node.names)
|
|
16348
|
+
|
|
16349
|
+
|
|
16350
|
+
file_path = sys.argv[1]
|
|
16351
|
+
class_name = sys.argv[2]
|
|
16352
|
+
template_name = f"{class_name}Template" if class_name else ""
|
|
16353
|
+
source = sys.stdin.read()
|
|
16354
|
+
issues = []
|
|
16355
|
+
|
|
16356
|
+
try:
|
|
16357
|
+
compile(source, file_path, "exec")
|
|
16358
|
+
except SyntaxError as error:
|
|
16359
|
+
issues.append({"path": issue_path(error), "message": error.msg})
|
|
16360
|
+
print(json.dumps({"issues": issues}))
|
|
16361
|
+
sys.exit(0)
|
|
16362
|
+
|
|
16363
|
+
if class_name:
|
|
16364
|
+
tree = ast.parse(source, filename=file_path)
|
|
16365
|
+
has_template_import = False
|
|
16366
|
+
has_form_class = False
|
|
16367
|
+
|
|
16368
|
+
for node in ast.walk(tree):
|
|
16369
|
+
if isinstance(node, ast.ImportFrom) and imports_template_from_anvil_designer(node, template_name):
|
|
16370
|
+
has_template_import = True
|
|
16371
|
+
elif isinstance(node, ast.ClassDef) and node.name == class_name:
|
|
16372
|
+
if any(base_name(base) == template_name for base in node.bases):
|
|
16373
|
+
has_form_class = True
|
|
16374
|
+
|
|
16375
|
+
if not has_template_import:
|
|
16376
|
+
issues.append({
|
|
16377
|
+
"path": "root",
|
|
16378
|
+
"message": f"missing {chr(96)}from ._anvil_designer import {template_name}{chr(96)}",
|
|
16379
|
+
})
|
|
16380
|
+
if not has_form_class:
|
|
16381
|
+
issues.append({
|
|
16382
|
+
"path": "root",
|
|
16383
|
+
"message": f"missing class {chr(96)}{class_name}{chr(96)} inheriting from {chr(96)}{template_name}{chr(96)}",
|
|
16384
|
+
})
|
|
16385
|
+
|
|
16386
|
+
print(json.dumps({"issues": issues}))
|
|
16387
|
+
`;
|
|
16388
|
+
function findPythonInterpreter() {
|
|
16389
|
+
const candidates = [
|
|
16390
|
+
process.env.ANVIL_PYTHON,
|
|
16391
|
+
"python3",
|
|
16392
|
+
"python"
|
|
16393
|
+
].filter((candidate)=>"string" == typeof candidate && candidate.length > 0);
|
|
16394
|
+
for (const candidate of candidates){
|
|
16395
|
+
const result = (0, external_child_process_namespaceObject.spawnSync)(candidate, [
|
|
16396
|
+
"--version"
|
|
16397
|
+
], {
|
|
16398
|
+
encoding: "utf8"
|
|
16399
|
+
});
|
|
16400
|
+
if (!result.error && 0 === result.status) return candidate;
|
|
16401
|
+
}
|
|
16402
|
+
return null;
|
|
16403
|
+
}
|
|
16404
|
+
function isClientFormPython(filePath) {
|
|
16405
|
+
if ("__init__.py" === external_path_default().basename(filePath)) {
|
|
16406
|
+
const formDir = external_path_default().dirname(filePath);
|
|
16407
|
+
return external_fs_.existsSync(external_path_default().join(formDir, "form_template.html")) || external_fs_.existsSync(external_path_default().join(formDir, "form_template.yaml"));
|
|
16408
|
+
}
|
|
16409
|
+
const parsed = external_path_default().parse(filePath);
|
|
16410
|
+
return external_fs_.existsSync(external_path_default().join(parsed.dir, `${parsed.name}.html`)) || external_fs_.existsSync(external_path_default().join(parsed.dir, `${parsed.name}.yaml`));
|
|
16411
|
+
}
|
|
16412
|
+
function expectedFormClassName(filePath) {
|
|
16413
|
+
if (!isClientFormPython(filePath)) return null;
|
|
16414
|
+
if ("__init__.py" === external_path_default().basename(filePath)) return external_path_default().basename(external_path_default().dirname(filePath));
|
|
16415
|
+
return external_path_default().basename(filePath, ".py");
|
|
16416
|
+
}
|
|
16417
|
+
function isUnderDirectory(filePath, directoryPath) {
|
|
16418
|
+
const relativePath = external_path_default().relative(directoryPath, filePath);
|
|
16419
|
+
return relativePath.length > 0 && !relativePath.startsWith("..") && !external_path_default().isAbsolute(relativePath);
|
|
16420
|
+
}
|
|
16421
|
+
function validatePython(filePath, fileContent, appRoot) {
|
|
16422
|
+
const interpreter = findPythonInterpreter();
|
|
16423
|
+
if (!interpreter) return {
|
|
16424
|
+
ok: false,
|
|
16425
|
+
message: "Python file could not be validated",
|
|
16426
|
+
issues: [
|
|
16427
|
+
{
|
|
16428
|
+
path: "root",
|
|
16429
|
+
message: "Python validation requires python3 or python on PATH, or ANVIL_PYTHON to be set"
|
|
16430
|
+
}
|
|
16431
|
+
]
|
|
16432
|
+
};
|
|
16433
|
+
const absolutePath = external_path_default().resolve(filePath);
|
|
16434
|
+
const clientCodeRoot = appRoot ? external_path_default().join(appRoot, "client_code") : null;
|
|
16435
|
+
const formClassName = clientCodeRoot && isUnderDirectory(absolutePath, clientCodeRoot) ? expectedFormClassName(absolutePath) : null;
|
|
16436
|
+
const result = (0, external_child_process_namespaceObject.spawnSync)(interpreter, [
|
|
16437
|
+
"-c",
|
|
16438
|
+
PYTHON_VALIDATOR_SCRIPT,
|
|
16439
|
+
filePath,
|
|
16440
|
+
formClassName ?? ""
|
|
16441
|
+
], {
|
|
16442
|
+
encoding: "utf8",
|
|
16443
|
+
input: fileContent,
|
|
16444
|
+
maxBuffer: 1048576
|
|
16445
|
+
});
|
|
16446
|
+
if (result.error || 0 !== result.status) return {
|
|
16447
|
+
ok: false,
|
|
16448
|
+
message: "Python file could not be validated",
|
|
16449
|
+
issues: [
|
|
16450
|
+
{
|
|
16451
|
+
path: "root",
|
|
16452
|
+
message: result.error?.message ?? (result.stderr.trim() || "Python validator failed")
|
|
16453
|
+
}
|
|
16454
|
+
]
|
|
16455
|
+
};
|
|
16456
|
+
let parsed;
|
|
16457
|
+
try {
|
|
16458
|
+
parsed = JSON.parse(result.stdout);
|
|
16459
|
+
} catch (error) {
|
|
16460
|
+
return {
|
|
16461
|
+
ok: false,
|
|
16462
|
+
message: "Python file could not be validated",
|
|
16463
|
+
issues: [
|
|
16464
|
+
{
|
|
16465
|
+
path: "root",
|
|
16466
|
+
message: error instanceof Error ? error.message : String(error)
|
|
16467
|
+
}
|
|
16468
|
+
]
|
|
16469
|
+
};
|
|
16470
|
+
}
|
|
16471
|
+
const issues = parsed.issues ?? [];
|
|
16472
|
+
if (issues.length > 0) return {
|
|
16473
|
+
ok: false,
|
|
16474
|
+
message: "Python file is invalid for Anvil",
|
|
16475
|
+
issues
|
|
16476
|
+
};
|
|
16477
|
+
return {
|
|
16478
|
+
ok: true
|
|
16479
|
+
};
|
|
16480
|
+
}
|
|
16281
16481
|
const validators_IDENTIFIER_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
16282
16482
|
const DB_ACCESS_LEVELS = new Set([
|
|
16283
16483
|
"none",
|
|
@@ -16675,6 +16875,13 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
16675
16875
|
const relativeToClientCode = normalizedPath.slice(startIndex + clientCodePrefix.length);
|
|
16676
16876
|
if (relativeToClientCode.endsWith(".yaml")) return "form_template.yaml";
|
|
16677
16877
|
if (relativeToClientCode.endsWith(".html")) return "form_template.html";
|
|
16878
|
+
if (relativeToClientCode.endsWith(".py")) return "python";
|
|
16879
|
+
}
|
|
16880
|
+
const serverCodePrefix = "server_code/";
|
|
16881
|
+
if (normalizedPath.startsWith(serverCodePrefix) || normalizedPath.includes(`/${serverCodePrefix}`)) {
|
|
16882
|
+
const startIndex = normalizedPath.lastIndexOf(serverCodePrefix);
|
|
16883
|
+
const relativeToServerCode = normalizedPath.slice(startIndex + serverCodePrefix.length);
|
|
16884
|
+
if (relativeToServerCode.endsWith(".py")) return "python";
|
|
16678
16885
|
}
|
|
16679
16886
|
return null;
|
|
16680
16887
|
}
|
|
@@ -16782,6 +16989,35 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
16782
16989
|
target
|
|
16783
16990
|
};
|
|
16784
16991
|
}
|
|
16992
|
+
if ("python" === target) {
|
|
16993
|
+
if (existingPath && appRoot) {
|
|
16994
|
+
const clientCodeRoot = external_path_default().join(appRoot, "client_code");
|
|
16995
|
+
const serverCodeRoot = external_path_default().join(appRoot, "server_code");
|
|
16996
|
+
const relativeToClientCode = external_path_default().relative(clientCodeRoot, existingPath);
|
|
16997
|
+
const relativeToServerCode = external_path_default().relative(serverCodeRoot, existingPath);
|
|
16998
|
+
const isInsideClientCode = relativeToClientCode.length > 0 && !relativeToClientCode.startsWith("..") && !external_path_default().isAbsolute(relativeToClientCode);
|
|
16999
|
+
const isInsideServerCode = relativeToServerCode.length > 0 && !relativeToServerCode.startsWith("..") && !external_path_default().isAbsolute(relativeToServerCode);
|
|
17000
|
+
if (!isInsideClientCode && !isInsideServerCode || !existingPath.endsWith(".py")) return {
|
|
17001
|
+
ok: false,
|
|
17002
|
+
target,
|
|
17003
|
+
message: "File is not an Anvil app Python file",
|
|
17004
|
+
issues: [
|
|
17005
|
+
{
|
|
17006
|
+
path: "",
|
|
17007
|
+
message: `expected a .py file under ${clientCodeRoot} or ${serverCodeRoot}`
|
|
17008
|
+
}
|
|
17009
|
+
]
|
|
17010
|
+
};
|
|
17011
|
+
}
|
|
17012
|
+
const result = validatePython(filePath, fileContent, appRoot);
|
|
17013
|
+
return result.ok ? {
|
|
17014
|
+
ok: true,
|
|
17015
|
+
target
|
|
17016
|
+
} : {
|
|
17017
|
+
...result,
|
|
17018
|
+
target
|
|
17019
|
+
};
|
|
17020
|
+
}
|
|
16785
17021
|
return {
|
|
16786
17022
|
ok: false,
|
|
16787
17023
|
target: "unknown",
|
|
@@ -16789,7 +17025,7 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
16789
17025
|
issues: [
|
|
16790
17026
|
{
|
|
16791
17027
|
path: "",
|
|
16792
|
-
message: "supported paths are anvil.yaml, client_code/**/form_template.yaml, client_code/**/*.yaml,
|
|
17028
|
+
message: "supported paths are anvil.yaml, client_code/**/form_template.yaml, client_code/**/*.yaml, client_code/**/*.html, client_code/**/*.py, and server_code/**/*.py"
|
|
16793
17029
|
}
|
|
16794
17030
|
]
|
|
16795
17031
|
};
|
|
@@ -17874,6 +18110,12 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
17874
18110
|
}
|
|
17875
18111
|
}
|
|
17876
18112
|
function authDiagnostic(error) {
|
|
18113
|
+
if ("token_refresh_failed" === error.type && error.message?.includes("invalid_grant")) return {
|
|
18114
|
+
code: error.type,
|
|
18115
|
+
recoverability: "requires_user_action",
|
|
18116
|
+
requiresUserAction: true,
|
|
18117
|
+
suggestedAction: "Your Anvil session auth has expired or was invalidated. Run anvil login or restart the agent/watch session, then retry."
|
|
18118
|
+
};
|
|
17877
18119
|
return {
|
|
17878
18120
|
code: error.type,
|
|
17879
18121
|
recoverability: "requires_user_action",
|
|
@@ -18790,7 +19032,7 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
18790
19032
|
getAuthToken: ()=>this.authToken,
|
|
18791
19033
|
setAuthToken: (token)=>{
|
|
18792
19034
|
this.authToken = token;
|
|
18793
|
-
this.wsClient?.updateAuthToken(token);
|
|
19035
|
+
this.wsClient?.updateAuthToken?.(token);
|
|
18794
19036
|
},
|
|
18795
19037
|
getCurrentBranch: ()=>this.currentBranch,
|
|
18796
19038
|
getCommitId: ()=>this.commitId,
|
|
@@ -18810,7 +19052,7 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
18810
19052
|
getAuthToken: ()=>this.authToken,
|
|
18811
19053
|
setAuthToken: (token)=>{
|
|
18812
19054
|
this.authToken = token;
|
|
18813
|
-
this.wsClient?.updateAuthToken(token);
|
|
19055
|
+
this.wsClient?.updateAuthToken?.(token);
|
|
18814
19056
|
},
|
|
18815
19057
|
getCurrentBranch: ()=>this.currentBranch,
|
|
18816
19058
|
getCommitId: ()=>this.commitId,
|
|
@@ -18978,6 +19220,8 @@ Promise.resolve(executeGitCredentialOperation(process.argv[2] || "get", {
|
|
|
18978
19220
|
]);
|
|
18979
19221
|
}
|
|
18980
19222
|
logger_logger.verbose(external_chalk_default().cyan(`Environment branch ${"snapshot" === source ? "snapshot" : "update"}: `) + external_chalk_default().bold(`${this.currentBranch} -> ${targetBranch}`));
|
|
19223
|
+
this.authToken = await auth_getValidAuthToken(this.anvilUrl, this.username);
|
|
19224
|
+
this.wsClient?.updateAuthToken?.(this.authToken);
|
|
18981
19225
|
const fetchUrl = anvil_api_getGitFetchUrl(this.appId, this.authToken, this.anvilUrl);
|
|
18982
19226
|
const envRemoteRef = environment_environmentRemoteRefForBranch(targetBranch);
|
|
18983
19227
|
await this.gitService.fetch(fetchUrl, environment_environmentFetchRefSpecForBranch(targetBranch));
|
package/dist/program.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"program.d.ts","sourceRoot":"","sources":["../src/program.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"program.d.ts","sourceRoot":"","sources":["../src/program.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AA2LpC,wBAAgB,qBAAqB,CAAC,QAAQ,EAAE,MAAM,CAAC,QAAQ,GAAG,MAAM,EAAE,CAezE;AAED,wBAAsB,mBAAmB,IAAI,OAAO,CAAC,IAAI,CAAC,CAwFzD;AAiBD,wBAAgB,YAAY,IAAI,OAAO,CAkGtC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../src/services/auth.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,EAAE,aAAa,EAAsC,MAAM,WAAW,CAAC;AAS9E,eAAO,MAAM,oBAAoB,eAAe,CAAC;AAMjD,oDAAoD;AACpD,MAAM,WAAW,SAAS;IACtB,YAAY,EAAE,MAAM,CAAC;IACrB,aAAa,EAAE,MAAM,CAAC;IACtB,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;CACjB;AAED,sBAAsB;AACtB,MAAM,WAAW,WAAW;IACxB,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;CACjB;AAED,uBAAuB;AACvB,MAAM,WAAW,YAAY;IACzB,SAAS,EAAE,OAAO,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,QAAQ;IACrB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,aAAa,CAAC;CACzB;
|
|
1
|
+
{"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../src/services/auth.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,EAAE,aAAa,EAAsC,MAAM,WAAW,CAAC;AAS9E,eAAO,MAAM,oBAAoB,eAAe,CAAC;AAMjD,oDAAoD;AACpD,MAAM,WAAW,SAAS;IACtB,YAAY,EAAE,MAAM,CAAC;IACrB,aAAa,EAAE,MAAM,CAAC;IACtB,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;CACjB;AAED,sBAAsB;AACtB,MAAM,WAAW,WAAW;IACxB,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;CACjB;AAED,uBAAuB;AACvB,MAAM,WAAW,YAAY;IACzB,SAAS,EAAE,OAAO,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,QAAQ;IACrB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,aAAa,CAAC;CACzB;AAaD,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,aAAa,GAAG,IAAI,CAMjG;AAED,wBAAgB,mBAAmB,IAAI,IAAI,CAE1C;AAYD,wBAAgB,cAAc,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAErD;AAQD;;;;GAIG;AACH,wBAAsB,UAAU,CAAC,SAAS,EAAE,MAAM,EAAE,QAAQ,SAAuB,GAAG,OAAO,CAAC,MAAM,CAAC,CAqBpG;AAED;;;GAGG;AACH,wBAAsB,kBAAkB,CACpC,YAAY,EAAE,MAAM,EACpB,QAAQ,SAAuB,EAC/B,QAAQ,SAAuB,GAChC,OAAO,CAAC,SAAS,CAAC,CA2BpB;AAMD;;GAEG;AACH,wBAAsB,eAAe,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAY3F;AAED;;;GAGG;AACH,wBAAsB,iBAAiB,CAAC,QAAQ,SAAuB,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAkC3G;AA8HD;;;GAGG;AACH,wBAAsB,oBAAoB,CACtC,QAAQ,EAAE,MAAM,EAChB,SAAS,EAAE;IAAE,YAAY,EAAE,MAAM,CAAC;IAAC,aAAa,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,GAC/F,OAAO,CAAC,WAAW,CAAC,CAetB;AAED;;GAEG;AACH,wBAAsB,MAAM,CAAC,QAAQ,CAAC,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC,CA0CxF;AAMD;;GAEG;AACH,wBAAsB,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAYtE;AAED;;GAEG;AACH,wBAAsB,qBAAqB,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC,CAW/D"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { ValidationIssue } from "./validators";
|
|
2
|
+
export type PythonValidationResult = {
|
|
3
|
+
ok: true;
|
|
4
|
+
} | {
|
|
5
|
+
ok: false;
|
|
6
|
+
message: string;
|
|
7
|
+
issues: ValidationIssue[];
|
|
8
|
+
};
|
|
9
|
+
export declare function validatePython(filePath: string, fileContent: string, appRoot: string | null): PythonValidationResult;
|
|
10
|
+
//# sourceMappingURL=validatePython.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validatePython.d.ts","sourceRoot":"","sources":["../src/validatePython.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAEpD,MAAM,MAAM,sBAAsB,GAC5B;IAAE,EAAE,EAAE,IAAI,CAAA;CAAE,GACZ;IACI,EAAE,EAAE,KAAK,CAAC;IACV,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,eAAe,EAAE,CAAC;CAC7B,CAAC;AAqHR,wBAAgB,cAAc,CAAC,QAAQ,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI,GAAG,sBAAsB,CAiEpH"}
|
package/dist/validators.d.ts
CHANGED
|
@@ -16,7 +16,7 @@ export type AnvilYamlValidationResult = {
|
|
|
16
16
|
message: string;
|
|
17
17
|
issues: ValidationIssue[];
|
|
18
18
|
};
|
|
19
|
-
export type ValidationTarget = "anvil.yaml" | "form_template.yaml" | "form_template.html";
|
|
19
|
+
export type ValidationTarget = "anvil.yaml" | "form_template.yaml" | "form_template.html" | "python";
|
|
20
20
|
export type ValidatePathResult = {
|
|
21
21
|
ok: true;
|
|
22
22
|
target: ValidationTarget;
|
|
@@ -43,6 +43,7 @@ export declare function findAnvilAppRoot(filePath: string): string | null;
|
|
|
43
43
|
* - `anvil.yaml`
|
|
44
44
|
* - any form YAML under `client_code/` with a `.yaml` suffix
|
|
45
45
|
* - any form HTML under `client_code/` with a `.html` suffix
|
|
46
|
+
* - any Python file under `client_code/` or `server_code/`
|
|
46
47
|
*/
|
|
47
48
|
export declare function validatePath(filePath: string, fileContent: string): ValidatePathResult;
|
|
48
49
|
//# sourceMappingURL=validators.d.ts.map
|
package/dist/validators.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"validators.d.ts","sourceRoot":"","sources":["../src/validators.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"validators.d.ts","sourceRoot":"","sources":["../src/validators.ts"],"names":[],"mappings":"AAYA,MAAM,MAAM,eAAe,GAAG;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;CACnB,CAAC;AAEF,MAAM,MAAM,4BAA4B,GAClC;IAAE,EAAE,EAAE,IAAI,CAAA;CAAE,GACZ;IACI,EAAE,EAAE,KAAK,CAAC;IACV,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,eAAe,EAAE,CAAC;CAC7B,CAAC;AAER,MAAM,MAAM,yBAAyB,GAC/B;IAAE,EAAE,EAAE,IAAI,CAAA;CAAE,GACZ;IACI,EAAE,EAAE,KAAK,CAAC;IACV,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,eAAe,EAAE,CAAC;CAC7B,CAAC;AAER,MAAM,MAAM,gBAAgB,GAAG,YAAY,GAAG,oBAAoB,GAAG,oBAAoB,GAAG,QAAQ,CAAC;AAErG,MAAM,MAAM,kBAAkB,GACxB;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,MAAM,EAAE,gBAAgB,CAAA;CAAE,GACtC;IACI,EAAE,EAAE,KAAK,CAAC;IACV,MAAM,EAAE,gBAAgB,GAAG,SAAS,CAAC;IACrC,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,eAAe,EAAE,CAAC;CAC7B,CAAC;AAoER,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,UAEhD;AAgBD;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,WAAW,EAAE,MAAM,GAAG,4BAA4B,CA0BtF;AAuZD;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,WAAW,EAAE,MAAM,GAAG,yBAAyB,CAkFhF;AAED,iBAAS,qBAAqB,CAAC,QAAQ,EAAE,MAAM,GAAG,gBAAgB,GAAG,IAAI,CA4BxE;AAED,OAAO,EAAE,qBAAqB,EAAE,CAAC;AAOjC,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAkBhE;AAED;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,QAAQ,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,kBAAkB,CAoItF"}
|