ic-mops 2.14.1 → 2.15.1
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/CHANGELOG.md +22 -0
- package/bundle/cli.tgz +0 -0
- package/cli.ts +149 -4
- package/commands/bench-replica.ts +2 -1
- package/commands/bench.ts +32 -9
- package/commands/build.ts +30 -77
- package/commands/check-stable.ts +4 -0
- package/commands/check.ts +26 -22
- package/commands/deployed.ts +162 -0
- package/commands/generate.ts +201 -0
- package/commands/lint.ts +6 -3
- package/dist/cli.js +76 -4
- package/dist/commands/bench-replica.js +3 -1
- package/dist/commands/bench.js +21 -4
- package/dist/commands/build.d.ts +6 -0
- package/dist/commands/build.js +26 -52
- package/dist/commands/check-stable.d.ts +2 -0
- package/dist/commands/check-stable.js +2 -2
- package/dist/commands/check.d.ts +2 -0
- package/dist/commands/check.js +20 -21
- package/dist/commands/deployed.d.ts +10 -0
- package/dist/commands/deployed.js +97 -0
- package/dist/commands/generate.d.ts +6 -0
- package/dist/commands/generate.js +124 -0
- package/dist/commands/lint.d.ts +2 -0
- package/dist/commands/lint.js +1 -1
- package/dist/helpers/autofix-motoko.d.ts +2 -0
- package/dist/helpers/autofix-motoko.js +71 -51
- package/dist/helpers/migrations.d.ts +1 -1
- package/dist/helpers/migrations.js +8 -4
- package/dist/helpers/moc-args.d.ts +25 -0
- package/dist/helpers/moc-args.js +58 -0
- package/dist/helpers/pocket-ic-client.d.ts +2 -2
- package/dist/helpers/pocket-ic-client.js +8 -4
- package/dist/package.json +6 -6
- package/dist/tests/check-fix-utf8.test.d.ts +1 -0
- package/dist/tests/check-fix-utf8.test.js +38 -0
- package/dist/tests/check-fix.test.js +14 -2
- package/dist/tests/check.test.js +21 -6
- package/dist/tests/deployed.test.d.ts +1 -0
- package/dist/tests/deployed.test.js +173 -0
- package/dist/tests/generate.test.d.ts +1 -0
- package/dist/tests/generate.test.js +107 -0
- package/dist/tests/helpers.d.ts +8 -0
- package/dist/tests/helpers.js +28 -3
- package/dist/tests/migrate.test.js +5 -19
- package/dist/types.d.ts +3 -0
- package/helpers/autofix-motoko.ts +96 -66
- package/helpers/migrations.ts +8 -3
- package/helpers/moc-args.ts +123 -0
- package/helpers/pocket-ic-client.ts +11 -5
- package/package.json +6 -6
- package/tests/__snapshots__/check-fix-utf8.test.ts.snap +27 -0
- package/tests/__snapshots__/check.test.ts.snap +4 -17
- package/tests/__snapshots__/deployed.test.ts.snap +10 -0
- package/tests/__snapshots__/generate.test.ts.snap +39 -0
- package/tests/check/fix-utf8/mops.toml +5 -0
- package/tests/check/fix-utf8/multibyte.mo +15 -0
- package/tests/check-fix-utf8.test.ts +51 -0
- package/tests/check-fix.test.ts +26 -2
- package/tests/check.test.ts +24 -11
- package/tests/deployed/basic/main.mo +4 -0
- package/tests/deployed/basic/mops.toml +8 -0
- package/tests/deployed/multi/bar.mo +1 -0
- package/tests/deployed/multi/foo.mo +1 -0
- package/tests/deployed/multi/mops.toml +11 -0
- package/tests/deployed.test.ts +249 -0
- package/tests/generate/basic/candid/bar.did +4 -0
- package/tests/generate/basic/mops.toml +12 -0
- package/tests/generate/basic/src/Bar.mo +5 -0
- package/tests/generate/basic/src/Foo.mo +5 -0
- package/tests/generate/error/mops.toml +8 -0
- package/tests/generate/error/src/Broken.mo +5 -0
- package/tests/generate.test.ts +140 -0
- package/tests/helpers.ts +31 -3
- package/tests/migrate.test.ts +7 -22
- package/types.ts +3 -0
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { readFile, writeFile } from "node:fs/promises";
|
|
2
|
-
import { resolve } from "node:path";
|
|
2
|
+
import { relative, resolve } from "node:path";
|
|
3
|
+
import chalk from "chalk";
|
|
3
4
|
import { execa } from "execa";
|
|
4
|
-
import { TextDocument
|
|
5
|
+
import { TextDocument } from "vscode-languageserver-textdocument";
|
|
5
6
|
export function parseDiagnostics(stdout) {
|
|
6
7
|
if (!stdout) {
|
|
7
8
|
return [];
|
|
@@ -24,25 +25,14 @@ function extractDiagnosticFixes(diagnostics) {
|
|
|
24
25
|
for (const diag of diagnostics) {
|
|
25
26
|
const editsByFile = new Map();
|
|
26
27
|
for (const span of diag.spans) {
|
|
27
|
-
if (span.suggestion_applicability
|
|
28
|
-
span.suggested_replacement
|
|
29
|
-
|
|
30
|
-
const edits = editsByFile.get(file) ?? [];
|
|
31
|
-
edits.push({
|
|
32
|
-
range: {
|
|
33
|
-
start: {
|
|
34
|
-
line: span.line_start - 1,
|
|
35
|
-
character: span.column_start - 1,
|
|
36
|
-
},
|
|
37
|
-
end: {
|
|
38
|
-
line: span.line_end - 1,
|
|
39
|
-
character: span.column_end - 1,
|
|
40
|
-
},
|
|
41
|
-
},
|
|
42
|
-
newText: span.suggested_replacement,
|
|
43
|
-
});
|
|
44
|
-
editsByFile.set(file, edits);
|
|
28
|
+
if (span.suggestion_applicability !== "MachineApplicable" ||
|
|
29
|
+
span.suggested_replacement === null) {
|
|
30
|
+
continue;
|
|
45
31
|
}
|
|
32
|
+
const file = resolve(span.file);
|
|
33
|
+
const edits = editsByFile.get(file) ?? [];
|
|
34
|
+
edits.push({ span, newText: span.suggested_replacement });
|
|
35
|
+
editsByFile.set(file, edits);
|
|
46
36
|
}
|
|
47
37
|
for (const [file, edits] of editsByFile) {
|
|
48
38
|
const existing = result.get(file) ?? [];
|
|
@@ -52,32 +42,47 @@ function extractDiagnosticFixes(diagnostics) {
|
|
|
52
42
|
}
|
|
53
43
|
return result;
|
|
54
44
|
}
|
|
55
|
-
function normalizeRange(range) {
|
|
56
|
-
const { start, end } = range;
|
|
57
|
-
if (start.line > end.line ||
|
|
58
|
-
(start.line === end.line && start.character > end.character)) {
|
|
59
|
-
return { start: end, end: start };
|
|
60
|
-
}
|
|
61
|
-
return range;
|
|
62
|
-
}
|
|
63
45
|
/**
|
|
64
|
-
* Applies diagnostic fixes to a
|
|
46
|
+
* Applies diagnostic fixes to a file's contents, processing each diagnostic as
|
|
65
47
|
* an atomic unit. If any edit from a diagnostic overlaps with an already-accepted
|
|
66
48
|
* edit, the entire diagnostic is skipped (picked up in subsequent iterations).
|
|
67
49
|
* Based on vscode-languageserver-textdocument's TextDocument.applyEdits.
|
|
50
|
+
*
|
|
51
|
+
* Edits prefer `byte_start`/`byte_end` (byte-accurate on any UTF-8 source);
|
|
52
|
+
* falls back to line+column for older moc, which mis-applies on non-ASCII
|
|
53
|
+
* lines since moc's `column_*` are bytes but LSP expects UTF-16 code units.
|
|
68
54
|
*/
|
|
69
|
-
function applyDiagnosticFixes(
|
|
55
|
+
function applyDiagnosticFixes(content, fixes) {
|
|
56
|
+
let buf = null;
|
|
57
|
+
const byteToOffset = (byteOffset) => {
|
|
58
|
+
buf ??= Buffer.from(content, "utf8");
|
|
59
|
+
return buf.subarray(0, byteOffset).toString("utf8").length;
|
|
60
|
+
};
|
|
61
|
+
let doc = null;
|
|
62
|
+
const toOffsetEdit = ({ span, newText }) => {
|
|
63
|
+
if (span.byte_start != null && span.byte_end != null) {
|
|
64
|
+
const [s, e] = span.byte_start <= span.byte_end
|
|
65
|
+
? [span.byte_start, span.byte_end]
|
|
66
|
+
: [span.byte_end, span.byte_start];
|
|
67
|
+
return { start: byteToOffset(s), end: byteToOffset(e), newText };
|
|
68
|
+
}
|
|
69
|
+
doc ??= TextDocument.create("inmemory://autofix", "motoko", 0, content);
|
|
70
|
+
const start = doc.offsetAt({
|
|
71
|
+
line: span.line_start - 1,
|
|
72
|
+
character: span.column_start - 1,
|
|
73
|
+
});
|
|
74
|
+
const end = doc.offsetAt({
|
|
75
|
+
line: span.line_end - 1,
|
|
76
|
+
character: span.column_end - 1,
|
|
77
|
+
});
|
|
78
|
+
return start <= end
|
|
79
|
+
? { start, end, newText }
|
|
80
|
+
: { start: end, end: start, newText };
|
|
81
|
+
};
|
|
70
82
|
const acceptedEdits = [];
|
|
71
83
|
const appliedCodes = [];
|
|
72
84
|
for (const fix of fixes) {
|
|
73
|
-
const offsets = fix.edits.map(
|
|
74
|
-
const range = normalizeRange(e.range);
|
|
75
|
-
return {
|
|
76
|
-
start: doc.offsetAt(range.start),
|
|
77
|
-
end: doc.offsetAt(range.end),
|
|
78
|
-
newText: e.newText,
|
|
79
|
-
};
|
|
80
|
-
});
|
|
85
|
+
const offsets = fix.edits.map(toOffsetEdit);
|
|
81
86
|
const overlaps = offsets.some((o) => acceptedEdits.some((a) => o.start < a.end && o.end > a.start));
|
|
82
87
|
if (overlaps) {
|
|
83
88
|
continue;
|
|
@@ -86,7 +91,6 @@ function applyDiagnosticFixes(doc, fixes) {
|
|
|
86
91
|
appliedCodes.push(fix.code);
|
|
87
92
|
}
|
|
88
93
|
acceptedEdits.sort((a, b) => a.start - b.start);
|
|
89
|
-
const text = doc.getText();
|
|
90
94
|
const spans = [];
|
|
91
95
|
let lastOffset = 0;
|
|
92
96
|
for (const edit of acceptedEdits) {
|
|
@@ -94,41 +98,57 @@ function applyDiagnosticFixes(doc, fixes) {
|
|
|
94
98
|
continue;
|
|
95
99
|
}
|
|
96
100
|
if (edit.start > lastOffset) {
|
|
97
|
-
spans.push(
|
|
101
|
+
spans.push(content.substring(lastOffset, edit.start));
|
|
98
102
|
}
|
|
99
103
|
if (edit.newText.length) {
|
|
100
104
|
spans.push(edit.newText);
|
|
101
105
|
}
|
|
102
106
|
lastOffset = edit.end;
|
|
103
107
|
}
|
|
104
|
-
spans.push(
|
|
108
|
+
spans.push(content.substring(lastOffset));
|
|
105
109
|
return { text: spans.join(""), appliedCodes };
|
|
106
110
|
}
|
|
107
111
|
const MAX_FIX_ITERATIONS = 10;
|
|
108
112
|
export async function autofixMotoko(mocPath, files, mocArgs) {
|
|
109
113
|
const fixedFilesCodes = new Map();
|
|
114
|
+
// Frozen migration files are often chmod'd read-only; warn once and skip
|
|
115
|
+
// them rather than aborting the whole run on EACCES/EPERM.
|
|
116
|
+
const readOnlySkipped = new Set();
|
|
110
117
|
for (let iteration = 0; iteration < MAX_FIX_ITERATIONS; iteration++) {
|
|
111
118
|
const fixesByFile = new Map();
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
+
for (const file of files) {
|
|
120
|
+
const result = await execa(mocPath, [file, ...mocArgs, "--error-format=json"], { stdio: "pipe", reject: false });
|
|
121
|
+
const diagnostics = parseDiagnostics(result.stdout);
|
|
122
|
+
for (const [targetFile, fixes] of extractDiagnosticFixes(diagnostics)) {
|
|
123
|
+
const existing = fixesByFile.get(targetFile) ?? [];
|
|
124
|
+
existing.push(...fixes);
|
|
125
|
+
fixesByFile.set(targetFile, existing);
|
|
126
|
+
}
|
|
119
127
|
}
|
|
120
128
|
if (fixesByFile.size === 0) {
|
|
121
129
|
break;
|
|
122
130
|
}
|
|
123
131
|
let progress = false;
|
|
124
132
|
for (const [file, fixes] of fixesByFile) {
|
|
133
|
+
if (readOnlySkipped.has(file)) {
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
125
136
|
const original = await readFile(file, "utf-8");
|
|
126
|
-
const
|
|
127
|
-
const { text: result, appliedCodes } = applyDiagnosticFixes(doc, fixes);
|
|
137
|
+
const { text: result, appliedCodes } = applyDiagnosticFixes(original, fixes);
|
|
128
138
|
if (result === original) {
|
|
129
139
|
continue;
|
|
130
140
|
}
|
|
131
|
-
|
|
141
|
+
try {
|
|
142
|
+
await writeFile(file, result, "utf-8");
|
|
143
|
+
}
|
|
144
|
+
catch (err) {
|
|
145
|
+
if (err?.code === "EACCES" || err?.code === "EPERM") {
|
|
146
|
+
readOnlySkipped.add(file);
|
|
147
|
+
console.warn(chalk.yellow(`Skipped read-only file ${relative(process.cwd(), file)} (${appliedCodes.length} fix(es) not applied)`));
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
throw err;
|
|
151
|
+
}
|
|
132
152
|
progress = true;
|
|
133
153
|
const existing = fixedFilesCodes.get(file) ?? [];
|
|
134
154
|
existing.push(...appliedCodes);
|
|
@@ -7,7 +7,7 @@ export declare function getMigrationFiles(dir: string): string[];
|
|
|
7
7
|
export declare function getNextMigrationFile(nextDir: string): string | null;
|
|
8
8
|
export declare function validateNextMigrationOrder(chainDirOrFiles: string | string[], nextFile: string): void;
|
|
9
9
|
export declare function validateMigrationsConfig(migrations: MigrationsConfig, canisterName: string): void;
|
|
10
|
-
export declare function prepareMigrationArgs(migrations: MigrationsConfig | undefined, canisterName: string, mode: "check" | "build", verbose?: boolean): Promise<MigrationArgsResult>;
|
|
10
|
+
export declare function prepareMigrationArgs(migrations: MigrationsConfig | undefined, canisterName: string, mode: "check" | "build", verbose?: boolean, ignoreLimit?: boolean): Promise<MigrationArgsResult>;
|
|
11
11
|
/**
|
|
12
12
|
* Absolute paths of chain migration files that `mops lint` should skip,
|
|
13
13
|
* mirroring the `check-limit` trimming applied to `moc` during `mops check`.
|
|
@@ -68,7 +68,7 @@ export function validateMigrationsConfig(migrations, canisterName) {
|
|
|
68
68
|
* for moc) and `getTrimmedMigrationFiles` (which feeds `excludedChainFiles`
|
|
69
69
|
* to lint).
|
|
70
70
|
*/
|
|
71
|
-
function resolveMigrationChain(migrations, canisterName, mode) {
|
|
71
|
+
function resolveMigrationChain(migrations, canisterName, mode, ignoreLimit = false) {
|
|
72
72
|
validateMigrationsConfig(migrations, canisterName);
|
|
73
73
|
const chainDir = resolveConfigPath(migrations.chain);
|
|
74
74
|
const nextDir = migrations.next
|
|
@@ -91,7 +91,11 @@ function resolveMigrationChain(migrations, canisterName, mode) {
|
|
|
91
91
|
if (nextFile && nextDir) {
|
|
92
92
|
all.push({ file: nextFile, dir: nextDir });
|
|
93
93
|
}
|
|
94
|
-
const limit =
|
|
94
|
+
const limit = ignoreLimit
|
|
95
|
+
? undefined
|
|
96
|
+
: mode === "check"
|
|
97
|
+
? migrations["check-limit"]
|
|
98
|
+
: migrations["build-limit"];
|
|
95
99
|
const isTrimming = limit !== undefined && limit < all.length;
|
|
96
100
|
const included = isTrimming ? all.slice(-limit) : all;
|
|
97
101
|
// Dropped entries are always a chain-only prefix (next sorts last).
|
|
@@ -100,11 +104,11 @@ function resolveMigrationChain(migrations, canisterName, mode) {
|
|
|
100
104
|
.map((e) => resolve(e.dir, e.file));
|
|
101
105
|
return { chainDir, nextDir, included, excludedChainFiles, isTrimming };
|
|
102
106
|
}
|
|
103
|
-
export async function prepareMigrationArgs(migrations, canisterName, mode, verbose) {
|
|
107
|
+
export async function prepareMigrationArgs(migrations, canisterName, mode, verbose, ignoreLimit = false) {
|
|
104
108
|
if (!migrations) {
|
|
105
109
|
return { migrationArgs: [], cleanup: async () => { } };
|
|
106
110
|
}
|
|
107
|
-
const { chainDir, nextDir, included, excludedChainFiles, isTrimming } = resolveMigrationChain(migrations, canisterName, mode);
|
|
111
|
+
const { chainDir, nextDir, included, excludedChainFiles, isTrimming } = resolveMigrationChain(migrations, canisterName, mode, ignoreLimit);
|
|
108
112
|
const hasNext = included.some((e) => e.dir === nextDir);
|
|
109
113
|
const needsTempDir = hasNext || isTrimming;
|
|
110
114
|
if (!needsTempDir) {
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { CanisterConfig, Config } from "../types.js";
|
|
2
|
+
/** Single source of truth for "how moc is invoked for canister X" — keeps
|
|
3
|
+
* `mops build` and `mops generate candid` in lockstep so the auto-generated
|
|
4
|
+
* `.did` they each produce stays subtype-compatible with the curated one
|
|
5
|
+
* embedded by `mops build`. */
|
|
6
|
+
export interface PrepareMocArgsOptions {
|
|
7
|
+
/** Selects the migration trim limit (`build-limit` vs `check-limit`). */
|
|
8
|
+
mode: "check" | "build";
|
|
9
|
+
/** Per-flag hints used when warning about conflicting `args` entries. */
|
|
10
|
+
managedFlags: Record<string, string>;
|
|
11
|
+
/** Command label shown in the warning, e.g. "mops build". */
|
|
12
|
+
commandName: string;
|
|
13
|
+
verbose?: boolean;
|
|
14
|
+
extraArgs?: string[];
|
|
15
|
+
}
|
|
16
|
+
export interface PreparedMocArgs {
|
|
17
|
+
motokoPath: string;
|
|
18
|
+
/** sources + [moc].args + migration + [build].args + [canisters.x].args + CLI extras. */
|
|
19
|
+
args: string[];
|
|
20
|
+
/** Cleans up the migration staging dir; call from a `finally`. */
|
|
21
|
+
cleanup: () => Promise<void>;
|
|
22
|
+
}
|
|
23
|
+
export declare const BUILD_MANAGED_FLAGS: Record<string, string>;
|
|
24
|
+
export declare const GENERATE_CANDID_MANAGED_FLAGS: Record<string, string>;
|
|
25
|
+
export declare function prepareMocArgs(config: Config, canister: CanisterConfig, canisterName: string, options: PrepareMocArgsOptions): Promise<PreparedMocArgs>;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import { cliError } from "../error.js";
|
|
3
|
+
import { getGlobalMocArgs, resolveConfigPath } from "../mops.js";
|
|
4
|
+
import { sourcesArgs } from "../commands/sources.js";
|
|
5
|
+
import { prepareMigrationArgs } from "./migrations.js";
|
|
6
|
+
import { validateCanisterArgs } from "./resolve-canisters.js";
|
|
7
|
+
export const BUILD_MANAGED_FLAGS = {
|
|
8
|
+
"-o": "use [build].outputDir in mops.toml or --output flag instead",
|
|
9
|
+
"-c": "this flag is always set by mops build",
|
|
10
|
+
"--idl": "this flag is always set by mops build",
|
|
11
|
+
"--stable-types": "this flag is always set by mops build",
|
|
12
|
+
"--public-metadata": "this flag is managed by mops build",
|
|
13
|
+
};
|
|
14
|
+
export const GENERATE_CANDID_MANAGED_FLAGS = {
|
|
15
|
+
"-o": "use the --output flag on `mops generate candid` instead",
|
|
16
|
+
"-c": "this flag is incompatible with mops generate candid (would also emit .wasm)",
|
|
17
|
+
"--idl": "this flag is always set by mops generate candid",
|
|
18
|
+
"--stable-types": "this flag is incompatible with mops generate candid (would also emit .most)",
|
|
19
|
+
};
|
|
20
|
+
export async function prepareMocArgs(config, canister, canisterName, options) {
|
|
21
|
+
if (!canister.main) {
|
|
22
|
+
cliError(`No main file is specified for canister ${canisterName}`);
|
|
23
|
+
}
|
|
24
|
+
const motokoPath = resolveConfigPath(canister.main);
|
|
25
|
+
const migration = await prepareMigrationArgs(canister.migrations, canisterName, options.mode, options.verbose);
|
|
26
|
+
const args = [
|
|
27
|
+
...(await sourcesArgs()).flat(),
|
|
28
|
+
...getGlobalMocArgs(config),
|
|
29
|
+
...migration.migrationArgs,
|
|
30
|
+
...collectExtraArgs(config, canister, canisterName, options.managedFlags, options.commandName, options.extraArgs),
|
|
31
|
+
];
|
|
32
|
+
return { motokoPath, args, cleanup: migration.cleanup };
|
|
33
|
+
}
|
|
34
|
+
function collectExtraArgs(config, canister, canisterName, managedFlags, commandName, extraArgs) {
|
|
35
|
+
const args = [];
|
|
36
|
+
if (config.build?.args) {
|
|
37
|
+
if (typeof config.build.args === "string") {
|
|
38
|
+
cliError(`[build] config 'args' should be an array of strings in mops.toml config file`);
|
|
39
|
+
}
|
|
40
|
+
args.push(...config.build.args);
|
|
41
|
+
}
|
|
42
|
+
if (canister.args) {
|
|
43
|
+
validateCanisterArgs(canister, canisterName, config);
|
|
44
|
+
args.push(...canister.args);
|
|
45
|
+
}
|
|
46
|
+
if (extraArgs) {
|
|
47
|
+
args.push(...extraArgs);
|
|
48
|
+
}
|
|
49
|
+
const warned = new Set();
|
|
50
|
+
for (const arg of args) {
|
|
51
|
+
const hint = managedFlags[arg];
|
|
52
|
+
if (hint && !warned.has(arg)) {
|
|
53
|
+
warned.add(arg);
|
|
54
|
+
console.warn(chalk.yellow(`Warning: '${arg}' in args for canister ${canisterName} may conflict with ${commandName} — ${hint}`));
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return args;
|
|
58
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { PocketIc, PocketIcServer } from "pic-ic";
|
|
2
|
-
import { PocketIc as PocketIcModern, PocketIcServer as PocketIcServerModern,
|
|
1
|
+
import type { PocketIc, PocketIcServer } from "pic-ic";
|
|
2
|
+
import type { PocketIc as PocketIcModern, PocketIcServer as PocketIcServerModern, StartServerOptions } from "pic-js-mops";
|
|
3
3
|
export type AnyPocketIcServer = PocketIcServer | PocketIcServerModern;
|
|
4
4
|
export type AnyPocketIc = PocketIc | PocketIcModern;
|
|
5
5
|
export type AnySetupCanister = PocketIc["setupCanister"] & PocketIcModern["setupCanister"];
|
|
@@ -1,18 +1,22 @@
|
|
|
1
1
|
import semver from "semver";
|
|
2
|
-
import { PocketIc, PocketIcServer } from "pic-ic";
|
|
3
|
-
import { PocketIc as PocketIcModern, PocketIcServer as PocketIcServerModern, } from "pic-js-mops";
|
|
4
2
|
import { readConfig } from "../mops.js";
|
|
5
3
|
function isLegacy() {
|
|
6
4
|
let version = readConfig().toolchain?.["pocket-ic"];
|
|
7
5
|
return !!version && !!semver.valid(version) && semver.lt(version, "9.0.0");
|
|
8
6
|
}
|
|
9
7
|
export async function startPocketIc(options) {
|
|
8
|
+
// Imported lazily so commands that never start a replica don't load the
|
|
9
|
+
// PocketIC client. `pic-js-mops` ships ESM without `type: module`, which a
|
|
10
|
+
// static import fails to resolve under tsx (local dev); a dynamic import
|
|
11
|
+
// resolves it on every platform.
|
|
10
12
|
if (isLegacy()) {
|
|
13
|
+
const { PocketIc, PocketIcServer } = await import("pic-ic");
|
|
11
14
|
let server = await PocketIcServer.start(options);
|
|
12
15
|
let client = await PocketIc.create(server.getUrl());
|
|
13
16
|
return { server, client };
|
|
14
17
|
}
|
|
15
|
-
|
|
16
|
-
let
|
|
18
|
+
const { PocketIc, PocketIcServer } = await import("pic-js-mops");
|
|
19
|
+
let server = await PocketIcServer.start(options);
|
|
20
|
+
let client = await PocketIc.create(server.getUrl());
|
|
17
21
|
return { server, client };
|
|
18
22
|
}
|
package/dist/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ic-mops",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.15.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"bin": {
|
|
6
6
|
"mops": "bin/mops.js",
|
|
@@ -55,7 +55,7 @@
|
|
|
55
55
|
"markdown-table": "3.0.4",
|
|
56
56
|
"mdast-util-from-markdown": "2.0.2",
|
|
57
57
|
"mdast-util-to-markdown": "2.1.2",
|
|
58
|
-
"minimatch": "10.2.
|
|
58
|
+
"minimatch": "10.2.5",
|
|
59
59
|
"ncp": "2.0.0",
|
|
60
60
|
"octokit": "3.1.2",
|
|
61
61
|
"pem-file": "1.0.1",
|
|
@@ -69,7 +69,7 @@
|
|
|
69
69
|
"semver": "7.7.1",
|
|
70
70
|
"stream-to-promise": "3.0.0",
|
|
71
71
|
"string-width": "7.2.0",
|
|
72
|
-
"tar": "7.5.
|
|
72
|
+
"tar": "7.5.16",
|
|
73
73
|
"terminal-size": "4.0.0",
|
|
74
74
|
"vscode-languageserver-textdocument": "1.0.12"
|
|
75
75
|
},
|
|
@@ -85,13 +85,13 @@
|
|
|
85
85
|
"@types/proper-lockfile": "4.1.4",
|
|
86
86
|
"@types/semver": "7.5.8",
|
|
87
87
|
"@types/stream-to-promise": "2.2.4",
|
|
88
|
-
"@types/tar": "
|
|
89
|
-
"esbuild": "0.
|
|
88
|
+
"@types/tar": "7.0.87",
|
|
89
|
+
"esbuild": "0.28.1",
|
|
90
90
|
"eslint": "8.57.0",
|
|
91
91
|
"npm-run-all": "4.1.5",
|
|
92
92
|
"rexreplace": "7.1.13",
|
|
93
93
|
"ts-jest": "29.4.5",
|
|
94
|
-
"tsx": "4.
|
|
94
|
+
"tsx": "4.22.4",
|
|
95
95
|
"typescript": "5.9.2",
|
|
96
96
|
"wasm-pack": "0.15.0"
|
|
97
97
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { describe, expect, test, beforeAll } from "@jest/globals";
|
|
2
|
+
import { cpSync, readdirSync, readFileSync, unlinkSync } from "node:fs";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import { cli, normalizePaths } from "./helpers";
|
|
5
|
+
// Regression: --fix must apply byte-accurately on UTF-8 source. The fixture
|
|
6
|
+
// pins a moc version that emits `byte_start`/`byte_end` so the byte path is
|
|
7
|
+
// exercised; without those fields the fixer over-deletes on multi-byte lines
|
|
8
|
+
// (e.g. `Char.toNat32(c)` after a `"京"` on the same line losing its `)`).
|
|
9
|
+
describe("check --fix (utf-8 source)", () => {
|
|
10
|
+
const fixDir = path.join(import.meta.dirname, "check/fix-utf8");
|
|
11
|
+
const runDir = path.join(fixDir, "run");
|
|
12
|
+
const warningFlags = "-W=M0236";
|
|
13
|
+
beforeAll(() => {
|
|
14
|
+
for (const file of readdirSync(runDir).filter((f) => f.endsWith(".mo"))) {
|
|
15
|
+
unlinkSync(path.join(runDir, file));
|
|
16
|
+
}
|
|
17
|
+
});
|
|
18
|
+
test("multi-byte characters do not corrupt the fix", async () => {
|
|
19
|
+
const src = "multibyte.mo";
|
|
20
|
+
const runFilePath = path.join(runDir, src);
|
|
21
|
+
cpSync(path.join(fixDir, src), runFilePath);
|
|
22
|
+
const fixResult = await cli(["check", runFilePath, "--fix", "--", warningFlags], { cwd: fixDir });
|
|
23
|
+
expect(normalizePaths(fixResult.stdout)).toMatchSnapshot("fix output");
|
|
24
|
+
const fixedContent = readFileSync(runFilePath, "utf-8");
|
|
25
|
+
expect(fixedContent).toMatchSnapshot("fixed file");
|
|
26
|
+
// Concrete byte-accuracy assertions: every Char.toNat32 call must be
|
|
27
|
+
// rewritten to dot notation, with the multi-byte literal before it intact
|
|
28
|
+
// and the trailing `)` preserved (the original regression: 京/💩 used to
|
|
29
|
+
// drop it).
|
|
30
|
+
expect(fixedContent).toContain('"A" # debug_show (c.toNat32());');
|
|
31
|
+
expect(fixedContent).toContain('"京" # debug_show (c.toNat32());');
|
|
32
|
+
expect(fixedContent).toContain('"💩" # debug_show (c.toNat32());');
|
|
33
|
+
expect(fixedContent).not.toMatch(/debug_show \(Char\.toNat32/);
|
|
34
|
+
// Verify no remaining M0236 warnings.
|
|
35
|
+
const afterResult = await cli(["check", runFilePath, "--", warningFlags, "--error-format=json"], { cwd: fixDir });
|
|
36
|
+
expect(afterResult.stdout).not.toContain('"code":"M0236"');
|
|
37
|
+
});
|
|
38
|
+
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { beforeAll, describe, expect, test } from "@jest/globals";
|
|
2
|
-
import { cpSync, readdirSync, readFileSync, unlinkSync } from "node:fs";
|
|
2
|
+
import { chmodSync, cpSync, readdirSync, readFileSync, unlinkSync, } from "node:fs";
|
|
3
3
|
import { mkdir, writeFile } from "node:fs/promises";
|
|
4
4
|
import path from "path";
|
|
5
5
|
import { lock } from "proper-lockfile";
|
|
@@ -19,7 +19,9 @@ describe("check --fix", () => {
|
|
|
19
19
|
const diagnosticFlags = [warningFlags, "--error-format=json"];
|
|
20
20
|
beforeAll(() => {
|
|
21
21
|
for (const file of readdirSync(runDir).filter((f) => f.endsWith(".mo"))) {
|
|
22
|
-
|
|
22
|
+
const p = path.join(runDir, file);
|
|
23
|
+
chmodSync(p, 0o644);
|
|
24
|
+
unlinkSync(p);
|
|
23
25
|
}
|
|
24
26
|
});
|
|
25
27
|
function copyFixture(file) {
|
|
@@ -86,6 +88,16 @@ describe("check --fix", () => {
|
|
|
86
88
|
expect(fixResult.stdout).toContain("1 fix applied");
|
|
87
89
|
expect(readFileSync(runFilePath, "utf-8")).not.toContain("<Nat>");
|
|
88
90
|
});
|
|
91
|
+
test("read-only file is skipped, not crashed", async () => {
|
|
92
|
+
const runFilePath = copyFixture("M0223.mo");
|
|
93
|
+
const before = readFileSync(runFilePath, "utf-8");
|
|
94
|
+
chmodSync(runFilePath, 0o444);
|
|
95
|
+
const result = await cli(["check", runFilePath, "--fix", "--", warningFlags], { cwd: fixDir });
|
|
96
|
+
expect(result.exitCode).toBe(0);
|
|
97
|
+
expect(result.stderr).toMatch(/Skipped read-only file/);
|
|
98
|
+
// File left untouched since the fix couldn't be written.
|
|
99
|
+
expect(readFileSync(runFilePath, "utf-8")).toBe(before);
|
|
100
|
+
});
|
|
89
101
|
test("verbose", async () => {
|
|
90
102
|
const result = await cli(["check", "Ok.mo", "--fix", "--verbose"], {
|
|
91
103
|
cwd: fixDir,
|
package/dist/tests/check.test.js
CHANGED
|
@@ -12,12 +12,6 @@ describe("check", () => {
|
|
|
12
12
|
await cliSnapshot(["check", "Error.mo"], { cwd }, 1);
|
|
13
13
|
await cliSnapshot(["check", "Ok.mo", "Error.mo"], { cwd }, 1);
|
|
14
14
|
});
|
|
15
|
-
// The verbose snapshot shows a single "moc ... [both files]" line, proving the
|
|
16
|
-
// whole set is checked in one invocation rather than one moc call per file.
|
|
17
|
-
test("multiple files in a single invocation", async () => {
|
|
18
|
-
const cwd = path.join(import.meta.dirname, "check/success");
|
|
19
|
-
await cliSnapshot(["check", "Ok.mo", "Warning.mo", "--verbose"], { cwd }, 0);
|
|
20
|
-
});
|
|
21
15
|
test("warning", async () => {
|
|
22
16
|
const cwd = path.join(import.meta.dirname, "check/success");
|
|
23
17
|
const result = await cliSnapshot(["check", "Warning.mo"], { cwd }, 0);
|
|
@@ -143,4 +137,25 @@ describe("check", () => {
|
|
|
143
137
|
expect(result.exitCode).toBe(0);
|
|
144
138
|
expect(result.stdout).toMatch(/✓ Lint fixes applied/);
|
|
145
139
|
});
|
|
140
|
+
// The migrations-chain fixture has 4 migrations with check-limit=3, so the
|
|
141
|
+
// oldest is trimmed by default (staged dir + M0254 suppression). --no-check-limit
|
|
142
|
+
// must point moc at the real chain dir and drop the trimming side effects.
|
|
143
|
+
const migrationsChain = "check-stable/migrations-chain";
|
|
144
|
+
test("check-limit trims the migration chain by default", async () => {
|
|
145
|
+
const cwd = path.join(import.meta.dirname, migrationsChain);
|
|
146
|
+
const result = await cli(["check", "--verbose"], { cwd });
|
|
147
|
+
expect(result.exitCode).toBe(0);
|
|
148
|
+
expect(result.stdout).toMatch(/--enhanced-migration=[^"]*\.migrations-/);
|
|
149
|
+
expect(result.stdout).toMatch(/-A=M0254/);
|
|
150
|
+
});
|
|
151
|
+
test("--no-check-limit uses the full migration chain", async () => {
|
|
152
|
+
const cwd = path.join(import.meta.dirname, migrationsChain);
|
|
153
|
+
const result = await cli(["check", "--verbose", "--no-check-limit"], {
|
|
154
|
+
cwd,
|
|
155
|
+
});
|
|
156
|
+
expect(result.exitCode).toBe(0);
|
|
157
|
+
expect(result.stdout).toMatch(/--enhanced-migration=/);
|
|
158
|
+
expect(result.stdout).not.toMatch(/\.migrations-/);
|
|
159
|
+
expect(result.stdout).not.toMatch(/-A=M0254/);
|
|
160
|
+
});
|
|
146
161
|
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|