@lotics/cli 0.88.0 → 0.89.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/dist/render_page.js +100 -100
- package/dist/src/cli.js +162 -100
- package/dist/{client.d.ts → src/client.d.ts} +9 -1
- package/dist/src/package_content_types.d.ts +70 -0
- package/dist/src/package_content_types.js +12 -0
- package/package.json +2 -2
- package/dist/app_commands.d.ts +0 -401
- package/dist/app_commands.js +0 -1520
- package/dist/app_commands.test.d.ts +0 -1
- package/dist/app_commands.test.js +0 -898
- package/dist/app_workflow_check.d.ts +0 -77
- package/dist/app_workflow_check.js +0 -169
- package/dist/app_workflow_check.test.d.ts +0 -1
- package/dist/app_workflow_check.test.js +0 -166
- package/dist/args.d.ts +0 -42
- package/dist/args.js +0 -121
- package/dist/args.test.d.ts +0 -1
- package/dist/args.test.js +0 -88
- package/dist/child_env.d.ts +0 -13
- package/dist/child_env.js +0 -24
- package/dist/cli.d.ts +0 -2
- package/dist/cli.js +0 -1095
- package/dist/cli_dispatch.test.d.ts +0 -1
- package/dist/cli_dispatch.test.js +0 -89
- package/dist/client.test.d.ts +0 -1
- package/dist/client.test.js +0 -47
- package/dist/config.d.ts +0 -124
- package/dist/config.js +0 -315
- package/dist/config.test.d.ts +0 -1
- package/dist/config.test.js +0 -319
- package/dist/dev/file_relay.d.ts +0 -39
- package/dist/dev/file_relay.js +0 -87
- package/dist/dev/file_relay.test.d.ts +0 -1
- package/dist/dev/file_relay.test.js +0 -87
- package/dist/dev/rpc_handler.d.ts +0 -21
- package/dist/dev/rpc_handler.js +0 -166
- package/dist/dev/rpc_handler.test.d.ts +0 -1
- package/dist/dev/rpc_handler.test.js +0 -107
- package/dist/dev/server.d.ts +0 -75
- package/dist/dev/server.js +0 -443
- package/dist/dev/upload_relay.d.ts +0 -36
- package/dist/dev/upload_relay.js +0 -61
- package/dist/dev/upload_relay.test.d.ts +0 -1
- package/dist/dev/upload_relay.test.js +0 -76
- package/dist/dev/wrapper_page.d.ts +0 -40
- package/dist/dev/wrapper_page.js +0 -335
- package/dist/docx.d.ts +0 -2
- package/dist/docx.js +0 -341
- package/dist/docx.test.d.ts +0 -1
- package/dist/docx.test.js +0 -145
- package/dist/file_command_io.d.ts +0 -12
- package/dist/file_command_io.js +0 -34
- package/dist/generate_app_agents_dts.d.ts +0 -16
- package/dist/generate_app_agents_dts.js +0 -62
- package/dist/generate_app_agents_dts.test.d.ts +0 -1
- package/dist/generate_app_agents_dts.test.js +0 -45
- package/dist/generate_app_fields.d.ts +0 -56
- package/dist/generate_app_fields.js +0 -148
- package/dist/generate_app_fields.test.d.ts +0 -1
- package/dist/generate_app_fields.test.js +0 -108
- package/dist/generate_app_queries_dts.d.ts +0 -21
- package/dist/generate_app_queries_dts.js +0 -57
- package/dist/generate_app_workflows_dts.d.ts +0 -39
- package/dist/generate_app_workflows_dts.js +0 -229
- package/dist/generate_app_workflows_dts.test.d.ts +0 -1
- package/dist/generate_app_workflows_dts.test.js +0 -109
- package/dist/generate_package_fields.d.ts +0 -49
- package/dist/generate_package_fields.js +0 -157
- package/dist/generate_package_fields.test.d.ts +0 -1
- package/dist/generate_package_fields.test.js +0 -59
- package/dist/inputs.d.ts +0 -38
- package/dist/inputs.js +0 -50
- package/dist/inputs.test.d.ts +0 -1
- package/dist/inputs.test.js +0 -89
- package/dist/preview.d.ts +0 -3
- package/dist/preview.js +0 -233
- package/dist/starter_template.d.ts +0 -64
- package/dist/starter_template.js +0 -753
- package/dist/starter_template.test.d.ts +0 -1
- package/dist/starter_template.test.js +0 -115
- package/dist/version.d.ts +0 -1
- package/dist/version.js +0 -6
- package/dist/xlsx.d.ts +0 -2
- package/dist/xlsx.js +0 -489
- package/dist/xlsx.test.d.ts +0 -1
- package/dist/xlsx.test.js +0 -131
- /package/dist/{client.js → src/client.js} +0 -0
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,115 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from "vitest";
|
|
2
|
-
import { buildStarterTemplate, STARTER_FALLBACK_SDK_VERSION, STARTER_FALLBACK_UI_VERSION, STARTER_REACT_NATIVE_VERSION, } from "./starter_template.js";
|
|
3
|
-
function fileNamed(files, path) {
|
|
4
|
-
const f = files.find((f) => f.path === path);
|
|
5
|
-
if (!f)
|
|
6
|
-
throw new Error(`Starter template missing file: ${path}`);
|
|
7
|
-
return f.content;
|
|
8
|
-
}
|
|
9
|
-
describe("buildStarterTemplate", () => {
|
|
10
|
-
const baseArgs = { app_name: "Demo", app_id: "app_test", workspace_id: "wsp_test" };
|
|
11
|
-
test("vite.config.ts pre-bundles RN deps with __DEV__ + .web.js for the dev server", () => {
|
|
12
|
-
const config = fileNamed(buildStarterTemplate(baseArgs), "vite.config.ts");
|
|
13
|
-
// The optimizer runs a SEPARATE esbuild pass that ignores `resolve.extensions`
|
|
14
|
-
// and the top-level `define`, so `__DEV__` + `.web.js` must be repeated here —
|
|
15
|
-
// else a pre-bundled RN dep (e.g. react-native-svg, which @lotics/ui charts
|
|
16
|
-
// use) resolves to its native build and `__DEV__` is undefined.
|
|
17
|
-
expect(config).toMatch(/optimizeDeps:\s*\{/);
|
|
18
|
-
expect(config).toContain('__DEV__: "false"');
|
|
19
|
-
expect(config).toContain('".web.js"');
|
|
20
|
-
});
|
|
21
|
-
test("vite.config.ts force-prebundles react-native-web (else a DatePicker/Picker blanks the dev iframe)", () => {
|
|
22
|
-
// RN-Web does `import normalizeColor from "@react-native/normalize-colors"`
|
|
23
|
-
// (nested CJS). Un-prebundled, the dev server serves it as ESM with no
|
|
24
|
-
// default → "does not provide an export named 'default'" + blank iframe the
|
|
25
|
-
// moment a colour-touching component mounts. Prebundling interops it.
|
|
26
|
-
const config = fileNamed(buildStarterTemplate(baseArgs), "vite.config.ts");
|
|
27
|
-
expect(config).toMatch(/include:\s*\[[^\]]*"react-native-web"/);
|
|
28
|
-
});
|
|
29
|
-
test("vite.config.ts defines the RN __DEV__ global (else the bundle throws at load → blank iframe)", () => {
|
|
30
|
-
// `__DEV__` is a Metro-injected global that RN-ecosystem deps reference at
|
|
31
|
-
// module-eval time (e.g. @react-native-picker's UnimplementedView, pulled in
|
|
32
|
-
// via @lotics/ui's DatePicker). Vite doesn't define it; undefined ⇒ a runtime
|
|
33
|
-
// ReferenceError that no typecheck/lint/build catches. Pin it so it can't
|
|
34
|
-
// silently regress and re-blank every app that imports such a dep.
|
|
35
|
-
const config = fileNamed(buildStarterTemplate(baseArgs), "vite.config.ts");
|
|
36
|
-
// Top-level define (covers the prod build + most dev modules)...
|
|
37
|
-
expect(config).toMatch(/define:\s*\{\s*__DEV__:\s*"false"/);
|
|
38
|
-
// ...and the dep-optimizer's separate esbuild pass (covers pre-bundled
|
|
39
|
-
// deps under `lotics app dev`).
|
|
40
|
-
expect(config).toMatch(/esbuildOptions:\s*\{\s*define:\s*\{\s*__DEV__:\s*"false"/);
|
|
41
|
-
});
|
|
42
|
-
test("vite.config.ts enables the vitest web optimizer with .web.js resolution (else tests rendering a Picker/DatePicker fail on Flow)", () => {
|
|
43
|
-
// Vitest pre-bundles deps with its OWN esbuild optimizer, which is OFF by
|
|
44
|
-
// default and ignores the top-level `optimizeDeps.resolveExtensions`. Without
|
|
45
|
-
// it enabled + given `.web.js` first, it grabs react-native's native `.js` /
|
|
46
|
-
// src Flow files (`import typeof`) that esbuild can't parse — so any test that
|
|
47
|
-
// renders a tree pulling a Picker/DatePicker dies with "Unexpected token 'typeof'".
|
|
48
|
-
const config = fileNamed(buildStarterTemplate(baseArgs), "vite.config.ts");
|
|
49
|
-
expect(config).toMatch(/test:\s*\{[\s\S]*environment:\s*"jsdom"/);
|
|
50
|
-
expect(config).toMatch(/optimizer:\s*\{\s*web:\s*\{\s*enabled:\s*true/);
|
|
51
|
-
// The web optimizer carries the same `.web.js`-first resolution as the build.
|
|
52
|
-
expect(config).toMatch(/web:\s*\{[\s\S]*resolveExtensions:[\s\S]*"\.web\.js"/);
|
|
53
|
-
});
|
|
54
|
-
test("vite.config.ts maps the RN `global` to globalThis (else rn-web Animated throws when a spring is torn down)", () => {
|
|
55
|
-
// react-native-web's Animated reads `global.cancelAnimationFrame` (free var)
|
|
56
|
-
// when a spring animation is interrupted — e.g. toggling @lotics/ui's Switch.
|
|
57
|
-
// Undefined in a web bundle ⇒ "global is not defined". The rollup build hits
|
|
58
|
-
// it; the dev esbuild path masks it, so it only surfaces in the deployed app
|
|
59
|
-
// and no typecheck/lint/build catches it. Pin both define sites.
|
|
60
|
-
const config = fileNamed(buildStarterTemplate(baseArgs), "vite.config.ts");
|
|
61
|
-
expect(config).toMatch(/define:\s*\{\s*__DEV__:\s*"false",\s*global:\s*"globalThis"/);
|
|
62
|
-
expect(config).toMatch(/esbuildOptions:\s*\{\s*define:\s*\{\s*__DEV__:\s*"false",\s*global:\s*"globalThis"/);
|
|
63
|
-
});
|
|
64
|
-
test("package.json pins react-native to the monorepo version (so apps typecheck @lotics/ui against the same RN types)", () => {
|
|
65
|
-
const pkg = JSON.parse(fileNamed(buildStarterTemplate(baseArgs), "package.json"));
|
|
66
|
-
expect(pkg.dependencies["react-native"]).toBe(STARTER_REACT_NATIVE_VERSION);
|
|
67
|
-
});
|
|
68
|
-
test("package.json uses caller-supplied versions for @lotics/ui and @lotics/app-sdk", () => {
|
|
69
|
-
const pkg = JSON.parse(fileNamed(buildStarterTemplate({ ...baseArgs, ui_version: "^9.9.9", sdk_version: "^2.2.2" }), "package.json"));
|
|
70
|
-
expect(pkg.dependencies["@lotics/ui"]).toBe("^9.9.9");
|
|
71
|
-
expect(pkg.dependencies["@lotics/app-sdk"]).toBe("^2.2.2");
|
|
72
|
-
});
|
|
73
|
-
test("package.json falls back to the static pins when versions are omitted", () => {
|
|
74
|
-
// Used when `lotics app create` cannot reach the npm registry to resolve
|
|
75
|
-
// `latest`. Keeps `app create` working offline at the cost of a stale pin.
|
|
76
|
-
const pkg = JSON.parse(fileNamed(buildStarterTemplate(baseArgs), "package.json"));
|
|
77
|
-
expect(pkg.dependencies["@lotics/ui"]).toBe(`^${STARTER_FALLBACK_UI_VERSION}`);
|
|
78
|
-
expect(pkg.dependencies["@lotics/app-sdk"]).toBe(`^${STARTER_FALLBACK_SDK_VERSION}`);
|
|
79
|
-
});
|
|
80
|
-
test("package.json includes react-router-dom for the in-app router example", () => {
|
|
81
|
-
const pkg = JSON.parse(fileNamed(buildStarterTemplate(baseArgs), "package.json"));
|
|
82
|
-
expect(pkg.dependencies["react-router-dom"]).toBeDefined();
|
|
83
|
-
});
|
|
84
|
-
test("App.tsx routes via the SDK's AppRouter (real addressable URLs, both modes)", () => {
|
|
85
|
-
// The scaffold demonstrates the recommended shape: the app uses react-router
|
|
86
|
-
// through `AppRouter`, which makes screens addressable URLs in both modes.
|
|
87
|
-
// Guards against the wiring being dropped back to a single screen.
|
|
88
|
-
const app = fileNamed(buildStarterTemplate(baseArgs), "src/App.tsx");
|
|
89
|
-
expect(app).toContain('from "react-router-dom"');
|
|
90
|
-
expect(app).toContain('from "@lotics/app-sdk/router"');
|
|
91
|
-
expect(app).toContain("<AppRouter routes={routes}");
|
|
92
|
-
});
|
|
93
|
-
test("vite.config.ts pins react-router-dom into the vitest web optimizer (else the routed App test splits the react instance)", () => {
|
|
94
|
-
// react-router-dom ships compiled dist JS; un-pinned, its hooks load a second
|
|
95
|
-
// react instance under vitest and the scaffold's App.test fails with "Invalid
|
|
96
|
-
// hook call" the moment it renders RouterProvider.
|
|
97
|
-
const config = fileNamed(buildStarterTemplate(baseArgs), "vite.config.ts");
|
|
98
|
-
expect(config).toMatch(/include:\s*\[[\s\S]*"react-router-dom"/);
|
|
99
|
-
});
|
|
100
|
-
test("main tsconfig excludes BOTH the workflow bodies and their per-alias globals (GAP-59 fix)", () => {
|
|
101
|
-
// npm run typecheck must skip src/workflows (DOM-lib skew) AND .lotics/workflows
|
|
102
|
-
// (the per-alias ambient `trigger` declarations collide across aliases and
|
|
103
|
-
// report false errors). Bodies are checked by `lotics app workflow check`.
|
|
104
|
-
const cfg = JSON.parse(fileNamed(buildStarterTemplate(baseArgs), "tsconfig.json"));
|
|
105
|
-
expect(cfg.exclude).toContain("src/workflows");
|
|
106
|
-
expect(cfg.exclude).toContain(".lotics/workflows");
|
|
107
|
-
});
|
|
108
|
-
test("no longer ships the all-in-one tsconfig.workflows.json (it collided per-alias globals)", () => {
|
|
109
|
-
// The dedicated config compiled every alias's globals into ONE program, so the
|
|
110
|
-
// ambient `trigger` declarations collided. `lotics app workflow check` replaces
|
|
111
|
-
// it with one isolated program per alias.
|
|
112
|
-
const files = buildStarterTemplate(baseArgs);
|
|
113
|
-
expect(files.find((f) => f.path === "tsconfig.workflows.json")).toBeUndefined();
|
|
114
|
-
});
|
|
115
|
-
});
|
package/dist/version.d.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export declare const VERSION: string;
|
package/dist/version.js
DELETED
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
import { readFileSync } from "node:fs";
|
|
2
|
-
import { fileURLToPath } from "node:url";
|
|
3
|
-
import path from "node:path";
|
|
4
|
-
const pkgPath = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../package.json");
|
|
5
|
-
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
6
|
-
export const VERSION = pkg.version;
|
package/dist/xlsx.d.ts
DELETED
package/dist/xlsx.js
DELETED
|
@@ -1,489 +0,0 @@
|
|
|
1
|
-
import fs from "node:fs";
|
|
2
|
-
import { fail, writeFileAtomic } from "./file_command_io.js";
|
|
3
|
-
import { parseExcelBuffer } from "@lotics/xlsx/excel_parser";
|
|
4
|
-
import { unzipXlsx } from "@lotics/xlsx/ooxml_zip";
|
|
5
|
-
import { exportWorkbook } from "@lotics/xlsx/xlsx_writer";
|
|
6
|
-
import { loadWorkbookFromSnapshot, WorkbookModel, SheetModel, StyleRegistry } from "@lotics/xlsx/workbook_model";
|
|
7
|
-
import { FormulaEngine } from "@lotics/xlsx/formula_engine";
|
|
8
|
-
import { refToRowCol, rowColToRef } from "@lotics/xlsx/excel_utils";
|
|
9
|
-
import { setCellValueCommand, clearRangeCommand, mergeCellsCommand, unmergeCellsCommand, addSheetCommand, deleteSheetCommand, renameSheetCommand, insertRowsCommand, deleteRowsCommand, insertColsCommand, deleteColsCommand, setRangeStyleCommand, } from "@lotics/xlsx/spreadsheet_commands";
|
|
10
|
-
function loadFile(filePath) {
|
|
11
|
-
if (!fs.existsSync(filePath))
|
|
12
|
-
fail(`File not found: ${filePath}`);
|
|
13
|
-
const buffer = fs.readFileSync(filePath);
|
|
14
|
-
const arrayBuffer = buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength);
|
|
15
|
-
const originalZip = unzipXlsx(arrayBuffer);
|
|
16
|
-
const parsed = parseExcelBuffer(arrayBuffer);
|
|
17
|
-
const workbook = loadWorkbookFromSnapshot(parsed);
|
|
18
|
-
return { workbook, originalZip };
|
|
19
|
-
}
|
|
20
|
-
function exportAndWrite(loaded, filePath) {
|
|
21
|
-
const bytes = exportWorkbook(loaded.workbook, loaded.originalZip);
|
|
22
|
-
writeFileAtomic(filePath, bytes);
|
|
23
|
-
}
|
|
24
|
-
function resolveSheetIndex(workbook, sheetName) {
|
|
25
|
-
const index = workbook.sheets.findIndex((s) => s.name === sheetName);
|
|
26
|
-
if (index < 0) {
|
|
27
|
-
const names = workbook.sheets.map((s) => `"${s.name}"`).join(", ");
|
|
28
|
-
fail(`Sheet not found: "${sheetName}". Available: ${names}`);
|
|
29
|
-
}
|
|
30
|
-
return index;
|
|
31
|
-
}
|
|
32
|
-
function parseRange(range) {
|
|
33
|
-
const [start, end] = range.includes(":") ? range.split(":") : [range, range];
|
|
34
|
-
const s = refToRowCol(start.toUpperCase());
|
|
35
|
-
const e = refToRowCol(end.toUpperCase());
|
|
36
|
-
if (!s || !e)
|
|
37
|
-
fail(`Invalid range: ${range}`);
|
|
38
|
-
return {
|
|
39
|
-
startRow: Math.min(s.row, e.row),
|
|
40
|
-
startCol: Math.min(s.col, e.col),
|
|
41
|
-
endRow: Math.max(s.row, e.row),
|
|
42
|
-
endCol: Math.max(s.col, e.col),
|
|
43
|
-
};
|
|
44
|
-
}
|
|
45
|
-
function parseInt10(s, name) {
|
|
46
|
-
if (s === undefined)
|
|
47
|
-
fail(`Missing ${name}`);
|
|
48
|
-
const n = parseInt(s, 10);
|
|
49
|
-
if (!Number.isFinite(n))
|
|
50
|
-
fail(`Invalid ${name}: ${s}`);
|
|
51
|
-
return n;
|
|
52
|
-
}
|
|
53
|
-
function lazyEngine(workbook) {
|
|
54
|
-
let engine;
|
|
55
|
-
return {
|
|
56
|
-
get() {
|
|
57
|
-
if (!engine) {
|
|
58
|
-
engine = new FormulaEngine(workbook);
|
|
59
|
-
engine.buildGraph();
|
|
60
|
-
}
|
|
61
|
-
return engine;
|
|
62
|
-
},
|
|
63
|
-
};
|
|
64
|
-
}
|
|
65
|
-
// =============================================================================
|
|
66
|
-
// JSON shape: ParsedSpreadsheet (read) and SimpleWorkbook (write)
|
|
67
|
-
// =============================================================================
|
|
68
|
-
function readToJson(filePath) {
|
|
69
|
-
const buffer = fs.readFileSync(filePath);
|
|
70
|
-
const arrayBuffer = buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength);
|
|
71
|
-
const parsed = parseExcelBuffer(arrayBuffer);
|
|
72
|
-
return {
|
|
73
|
-
activeSheetIndex: parsed.activeSheetIndex,
|
|
74
|
-
sheets: parsed.sheets.map((s) => ({
|
|
75
|
-
name: s.name,
|
|
76
|
-
totalRowCount: s.totalRowCount,
|
|
77
|
-
truncated: s.truncated,
|
|
78
|
-
cells: cellsFromParsedRows(s.rows),
|
|
79
|
-
merges: s.mergedCells.map((m) => `${rowColToRef(m.startRow, m.startCol)}:${rowColToRef(m.endRow, m.endCol)}`),
|
|
80
|
-
freeze: s.freezePane ? { row: s.freezePane.frozenRows, col: s.freezePane.frozenCols } : undefined,
|
|
81
|
-
})),
|
|
82
|
-
};
|
|
83
|
-
}
|
|
84
|
-
function cellsFromParsedRows(rows) {
|
|
85
|
-
const out = {};
|
|
86
|
-
for (const row of rows) {
|
|
87
|
-
for (const cell of row.cells) {
|
|
88
|
-
const ref = rowColToRef(row.index, cell.column);
|
|
89
|
-
const value = cell.typedValue ?? cell.value;
|
|
90
|
-
const entry = { value };
|
|
91
|
-
if (cell.formula)
|
|
92
|
-
entry.formula = cell.formula;
|
|
93
|
-
out[ref] = entry;
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
return out;
|
|
97
|
-
}
|
|
98
|
-
function buildWorkbookFromJson(input) {
|
|
99
|
-
if (!input || !Array.isArray(input.sheets) || input.sheets.length === 0) {
|
|
100
|
-
fail("Workbook JSON must include a non-empty `sheets` array");
|
|
101
|
-
}
|
|
102
|
-
for (let i = 0; i < input.sheets.length; i++)
|
|
103
|
-
validateSimpleSheet(input.sheets[i], i);
|
|
104
|
-
const styles = new StyleRegistry();
|
|
105
|
-
const sheets = input.sheets.map((s) => new SheetModel(s.name, styles));
|
|
106
|
-
const workbook = new WorkbookModel({ sheets, activeSheetIndex: 0, styles });
|
|
107
|
-
for (let i = 0; i < input.sheets.length; i++) {
|
|
108
|
-
applySheetCells(workbook, i, input.sheets[i]);
|
|
109
|
-
}
|
|
110
|
-
return workbook;
|
|
111
|
-
}
|
|
112
|
-
function validateSimpleSheet(s, index) {
|
|
113
|
-
if (!s || typeof s !== "object")
|
|
114
|
-
fail(`sheets[${index}] must be an object`);
|
|
115
|
-
if (typeof s.name !== "string" || s.name === "")
|
|
116
|
-
fail(`sheets[${index}].name must be a non-empty string`);
|
|
117
|
-
if (s.cells !== undefined && (typeof s.cells !== "object" || s.cells === null || Array.isArray(s.cells))) {
|
|
118
|
-
fail(`sheets[${index}].cells must be an object keyed by cell ref (e.g. {"A1": "value"})`);
|
|
119
|
-
}
|
|
120
|
-
if (s.merges !== undefined && !Array.isArray(s.merges)) {
|
|
121
|
-
fail(`sheets[${index}].merges must be an array of ranges (e.g. ["A1:B2"])`);
|
|
122
|
-
}
|
|
123
|
-
if (s.freeze !== undefined) {
|
|
124
|
-
if (typeof s.freeze !== "object" || s.freeze === null)
|
|
125
|
-
fail(`sheets[${index}].freeze must be {row, col}`);
|
|
126
|
-
if (typeof s.freeze.row !== "number" || typeof s.freeze.col !== "number") {
|
|
127
|
-
fail(`sheets[${index}].freeze.row and .col must be numbers`);
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
function applySheetCells(workbook, sheetIndex, def) {
|
|
132
|
-
const sheet = workbook.sheets[sheetIndex];
|
|
133
|
-
if (def.cells) {
|
|
134
|
-
for (const [ref, raw] of Object.entries(def.cells)) {
|
|
135
|
-
const cell = normalizeSimpleCell(raw);
|
|
136
|
-
const coord = refToRowCol(ref.toUpperCase());
|
|
137
|
-
if (!coord)
|
|
138
|
-
fail(`Invalid cell ref in sheet "${def.name}": ${ref}`);
|
|
139
|
-
const stringValue = cell.formula ? `=${cell.formula}` : valueToInput(cell.value);
|
|
140
|
-
const cmd = setCellValueCommand(workbook, undefined, sheetIndex, coord.row, coord.col, stringValue);
|
|
141
|
-
cmd.execute();
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
if (def.merges) {
|
|
145
|
-
for (const range of def.merges) {
|
|
146
|
-
const r = parseRange(range);
|
|
147
|
-
const cmd = mergeCellsCommand(workbook, sheetIndex, r.startRow, r.startCol, r.endRow, r.endCol);
|
|
148
|
-
cmd.execute();
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
if (def.freeze) {
|
|
152
|
-
sheet.freeze = { row: def.freeze.row, col: def.freeze.col };
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
function normalizeSimpleCell(raw) {
|
|
156
|
-
if (raw && typeof raw === "object" && !Array.isArray(raw)) {
|
|
157
|
-
return raw;
|
|
158
|
-
}
|
|
159
|
-
return { value: raw };
|
|
160
|
-
}
|
|
161
|
-
function valueToInput(value) {
|
|
162
|
-
if (value === null || value === undefined)
|
|
163
|
-
return "";
|
|
164
|
-
return String(value);
|
|
165
|
-
}
|
|
166
|
-
// =============================================================================
|
|
167
|
-
// Subcommand handlers
|
|
168
|
-
// =============================================================================
|
|
169
|
-
function xlsxRead(filePath) {
|
|
170
|
-
if (!filePath)
|
|
171
|
-
fail("Usage: lotics xlsx read <file>");
|
|
172
|
-
const data = readToJson(filePath);
|
|
173
|
-
console.log(JSON.stringify(data, null, 2));
|
|
174
|
-
}
|
|
175
|
-
function xlsxWrite(filePath, json) {
|
|
176
|
-
if (!filePath || !json)
|
|
177
|
-
fail("Usage: lotics xlsx write <file> '<json>'");
|
|
178
|
-
let parsed;
|
|
179
|
-
try {
|
|
180
|
-
parsed = JSON.parse(json);
|
|
181
|
-
}
|
|
182
|
-
catch (e) {
|
|
183
|
-
fail(`Invalid JSON: ${e instanceof Error ? e.message : String(e)}`);
|
|
184
|
-
}
|
|
185
|
-
const workbook = buildWorkbookFromJson(parsed);
|
|
186
|
-
const bytes = exportWorkbook(workbook);
|
|
187
|
-
writeFileAtomic(filePath, bytes);
|
|
188
|
-
}
|
|
189
|
-
function parseSheetRef(spec) {
|
|
190
|
-
const idx = spec.indexOf("!");
|
|
191
|
-
if (idx < 0)
|
|
192
|
-
fail(`Expected <sheet>!<ref>, got: ${spec}`);
|
|
193
|
-
return { sheet: spec.slice(0, idx), ref: spec.slice(idx + 1) };
|
|
194
|
-
}
|
|
195
|
-
function xlsxSetCell(filePath, rest) {
|
|
196
|
-
const [sheetRef, value] = rest;
|
|
197
|
-
if (!filePath || !sheetRef || value === undefined) {
|
|
198
|
-
fail("Usage: lotics xlsx set-cell <file> <sheet>!<ref> '<value>'");
|
|
199
|
-
}
|
|
200
|
-
const { sheet, ref } = parseSheetRef(sheetRef);
|
|
201
|
-
const coord = refToRowCol(ref.toUpperCase());
|
|
202
|
-
if (!coord)
|
|
203
|
-
fail(`Invalid cell ref: ${ref}`);
|
|
204
|
-
const loaded = loadFile(filePath);
|
|
205
|
-
const sheetIndex = resolveSheetIndex(loaded.workbook, sheet);
|
|
206
|
-
const engine = lazyEngine(loaded.workbook);
|
|
207
|
-
const cmd = setCellValueCommand(loaded.workbook, engine.get(), sheetIndex, coord.row, coord.col, value);
|
|
208
|
-
cmd.execute();
|
|
209
|
-
exportAndWrite(loaded, filePath);
|
|
210
|
-
}
|
|
211
|
-
function xlsxClearRange(filePath, rest) {
|
|
212
|
-
const [sheetRef] = rest;
|
|
213
|
-
if (!filePath || !sheetRef)
|
|
214
|
-
fail("Usage: lotics xlsx clear-range <file> <sheet>!<range>");
|
|
215
|
-
const { sheet, ref } = parseSheetRef(sheetRef);
|
|
216
|
-
const r = parseRange(ref);
|
|
217
|
-
const loaded = loadFile(filePath);
|
|
218
|
-
const sheetIndex = resolveSheetIndex(loaded.workbook, sheet);
|
|
219
|
-
const engine = lazyEngine(loaded.workbook);
|
|
220
|
-
const cmd = clearRangeCommand(loaded.workbook, engine.get(), sheetIndex, r.startRow, r.startCol, r.endRow, r.endCol);
|
|
221
|
-
cmd.execute();
|
|
222
|
-
exportAndWrite(loaded, filePath);
|
|
223
|
-
}
|
|
224
|
-
function xlsxMerge(filePath, rest) {
|
|
225
|
-
const [sheetRef] = rest;
|
|
226
|
-
if (!filePath || !sheetRef)
|
|
227
|
-
fail("Usage: lotics xlsx merge <file> <sheet>!<range>");
|
|
228
|
-
const { sheet, ref } = parseSheetRef(sheetRef);
|
|
229
|
-
const r = parseRange(ref);
|
|
230
|
-
const loaded = loadFile(filePath);
|
|
231
|
-
const sheetIndex = resolveSheetIndex(loaded.workbook, sheet);
|
|
232
|
-
const cmd = mergeCellsCommand(loaded.workbook, sheetIndex, r.startRow, r.startCol, r.endRow, r.endCol);
|
|
233
|
-
cmd.execute();
|
|
234
|
-
exportAndWrite(loaded, filePath);
|
|
235
|
-
}
|
|
236
|
-
function xlsxUnmerge(filePath, rest) {
|
|
237
|
-
const [sheetRef] = rest;
|
|
238
|
-
if (!filePath || !sheetRef)
|
|
239
|
-
fail("Usage: lotics xlsx unmerge <file> <sheet>!<range>");
|
|
240
|
-
const { sheet, ref } = parseSheetRef(sheetRef);
|
|
241
|
-
const r = parseRange(ref);
|
|
242
|
-
const mergeRef = `${rowColToRef(r.startRow, r.startCol)}:${rowColToRef(r.endRow, r.endCol)}`;
|
|
243
|
-
const loaded = loadFile(filePath);
|
|
244
|
-
const sheetIndex = resolveSheetIndex(loaded.workbook, sheet);
|
|
245
|
-
const cmd = unmergeCellsCommand(loaded.workbook, sheetIndex, mergeRef);
|
|
246
|
-
cmd.execute();
|
|
247
|
-
exportAndWrite(loaded, filePath);
|
|
248
|
-
}
|
|
249
|
-
function xlsxAddSheet(filePath, rest) {
|
|
250
|
-
const [name] = rest;
|
|
251
|
-
if (!filePath || !name)
|
|
252
|
-
fail("Usage: lotics xlsx add-sheet <file> <name>");
|
|
253
|
-
const loaded = loadFile(filePath);
|
|
254
|
-
const cmd = addSheetCommand(loaded.workbook, name);
|
|
255
|
-
cmd.execute();
|
|
256
|
-
exportAndWrite(loaded, filePath);
|
|
257
|
-
}
|
|
258
|
-
function xlsxDeleteSheet(filePath, rest) {
|
|
259
|
-
const [name] = rest;
|
|
260
|
-
if (!filePath || !name)
|
|
261
|
-
fail("Usage: lotics xlsx delete-sheet <file> <name>");
|
|
262
|
-
const loaded = loadFile(filePath);
|
|
263
|
-
const sheetIndex = resolveSheetIndex(loaded.workbook, name);
|
|
264
|
-
const cmd = deleteSheetCommand(loaded.workbook, sheetIndex);
|
|
265
|
-
cmd.execute();
|
|
266
|
-
exportAndWrite(loaded, filePath);
|
|
267
|
-
}
|
|
268
|
-
function xlsxRenameSheet(filePath, rest) {
|
|
269
|
-
const [oldName, newName] = rest;
|
|
270
|
-
if (!filePath || !oldName || !newName)
|
|
271
|
-
fail("Usage: lotics xlsx rename-sheet <file> <old> <new>");
|
|
272
|
-
const loaded = loadFile(filePath);
|
|
273
|
-
const sheetIndex = resolveSheetIndex(loaded.workbook, oldName);
|
|
274
|
-
const cmd = renameSheetCommand(loaded.workbook, sheetIndex, newName);
|
|
275
|
-
cmd.execute();
|
|
276
|
-
exportAndWrite(loaded, filePath);
|
|
277
|
-
}
|
|
278
|
-
function xlsxInsertRows(filePath, rest) {
|
|
279
|
-
const [sheetName, atStr, countStr] = rest;
|
|
280
|
-
if (!filePath || !sheetName)
|
|
281
|
-
fail("Usage: lotics xlsx insert-rows <file> <sheet> <at> <count>");
|
|
282
|
-
const at = parseInt10(atStr, "at");
|
|
283
|
-
const count = parseInt10(countStr, "count");
|
|
284
|
-
const loaded = loadFile(filePath);
|
|
285
|
-
const sheetIndex = resolveSheetIndex(loaded.workbook, sheetName);
|
|
286
|
-
const cmd = insertRowsCommand(loaded.workbook, sheetIndex, at, count);
|
|
287
|
-
cmd.execute();
|
|
288
|
-
exportAndWrite(loaded, filePath);
|
|
289
|
-
}
|
|
290
|
-
function xlsxDeleteRows(filePath, rest) {
|
|
291
|
-
const [sheetName, atStr, countStr] = rest;
|
|
292
|
-
if (!filePath || !sheetName)
|
|
293
|
-
fail("Usage: lotics xlsx delete-rows <file> <sheet> <at> <count>");
|
|
294
|
-
const at = parseInt10(atStr, "at");
|
|
295
|
-
const count = parseInt10(countStr, "count");
|
|
296
|
-
const loaded = loadFile(filePath);
|
|
297
|
-
const sheetIndex = resolveSheetIndex(loaded.workbook, sheetName);
|
|
298
|
-
const cmd = deleteRowsCommand(loaded.workbook, sheetIndex, at, count);
|
|
299
|
-
cmd.execute();
|
|
300
|
-
exportAndWrite(loaded, filePath);
|
|
301
|
-
}
|
|
302
|
-
function xlsxInsertCols(filePath, rest) {
|
|
303
|
-
const [sheetName, atStr, countStr] = rest;
|
|
304
|
-
if (!filePath || !sheetName)
|
|
305
|
-
fail("Usage: lotics xlsx insert-cols <file> <sheet> <at> <count>");
|
|
306
|
-
const at = parseInt10(atStr, "at");
|
|
307
|
-
const count = parseInt10(countStr, "count");
|
|
308
|
-
const loaded = loadFile(filePath);
|
|
309
|
-
const sheetIndex = resolveSheetIndex(loaded.workbook, sheetName);
|
|
310
|
-
const cmd = insertColsCommand(loaded.workbook, sheetIndex, at, count);
|
|
311
|
-
cmd.execute();
|
|
312
|
-
exportAndWrite(loaded, filePath);
|
|
313
|
-
}
|
|
314
|
-
function xlsxDeleteCols(filePath, rest) {
|
|
315
|
-
const [sheetName, atStr, countStr] = rest;
|
|
316
|
-
if (!filePath || !sheetName)
|
|
317
|
-
fail("Usage: lotics xlsx delete-cols <file> <sheet> <at> <count>");
|
|
318
|
-
const at = parseInt10(atStr, "at");
|
|
319
|
-
const count = parseInt10(countStr, "count");
|
|
320
|
-
const loaded = loadFile(filePath);
|
|
321
|
-
const sheetIndex = resolveSheetIndex(loaded.workbook, sheetName);
|
|
322
|
-
const cmd = deleteColsCommand(loaded.workbook, sheetIndex, at, count);
|
|
323
|
-
cmd.execute();
|
|
324
|
-
exportAndWrite(loaded, filePath);
|
|
325
|
-
}
|
|
326
|
-
function xlsxSetStyle(filePath, rest) {
|
|
327
|
-
const [sheetRef, styleJson] = rest;
|
|
328
|
-
if (!filePath || !sheetRef || !styleJson) {
|
|
329
|
-
fail("Usage: lotics xlsx set-style <file> <sheet>!<range> '<style-json>'");
|
|
330
|
-
}
|
|
331
|
-
const { sheet, ref } = parseSheetRef(sheetRef);
|
|
332
|
-
const r = parseRange(ref);
|
|
333
|
-
let style;
|
|
334
|
-
try {
|
|
335
|
-
style = JSON.parse(styleJson);
|
|
336
|
-
}
|
|
337
|
-
catch (e) {
|
|
338
|
-
fail(`Invalid style JSON: ${e instanceof Error ? e.message : String(e)}`);
|
|
339
|
-
}
|
|
340
|
-
const loaded = loadFile(filePath);
|
|
341
|
-
const sheetIndex = resolveSheetIndex(loaded.workbook, sheet);
|
|
342
|
-
const cmd = setRangeStyleCommand(loaded.workbook, sheetIndex, r.startRow, r.startCol, r.endRow, r.endCol, style);
|
|
343
|
-
cmd.execute();
|
|
344
|
-
exportAndWrite(loaded, filePath);
|
|
345
|
-
}
|
|
346
|
-
function xlsxBatch(filePath, json) {
|
|
347
|
-
if (!filePath || !json)
|
|
348
|
-
fail("Usage: lotics xlsx batch <file> '<json-array-of-ops>'");
|
|
349
|
-
let ops;
|
|
350
|
-
try {
|
|
351
|
-
ops = JSON.parse(json);
|
|
352
|
-
}
|
|
353
|
-
catch (e) {
|
|
354
|
-
fail(`Invalid batch JSON: ${e instanceof Error ? e.message : String(e)}`);
|
|
355
|
-
}
|
|
356
|
-
if (!Array.isArray(ops))
|
|
357
|
-
fail("Batch must be a JSON array of ops");
|
|
358
|
-
for (let i = 0; i < ops.length; i++) {
|
|
359
|
-
const op = ops[i];
|
|
360
|
-
if (!op || typeof op !== "object" || typeof op.op !== "string") {
|
|
361
|
-
fail(`[op ${i}] each op must be an object with an "op" string field`);
|
|
362
|
-
}
|
|
363
|
-
}
|
|
364
|
-
const loaded = loadFile(filePath);
|
|
365
|
-
const engine = lazyEngine(loaded.workbook);
|
|
366
|
-
for (let i = 0; i < ops.length; i++) {
|
|
367
|
-
const op = ops[i];
|
|
368
|
-
const cmd = buildBatchCommand(loaded.workbook, engine, op, i);
|
|
369
|
-
cmd.execute();
|
|
370
|
-
}
|
|
371
|
-
exportAndWrite(loaded, filePath);
|
|
372
|
-
}
|
|
373
|
-
function buildBatchCommand(workbook, engine, op, index) {
|
|
374
|
-
switch (op.op) {
|
|
375
|
-
case "set-cell": {
|
|
376
|
-
const sheetIndex = resolveSheetIndex(workbook, op.sheet);
|
|
377
|
-
const coord = refToRowCol(op.ref.toUpperCase());
|
|
378
|
-
if (!coord)
|
|
379
|
-
fail(`[op ${index}] Invalid cell ref: ${op.ref}`);
|
|
380
|
-
return setCellValueCommand(workbook, engine.get(), sheetIndex, coord.row, coord.col, op.value);
|
|
381
|
-
}
|
|
382
|
-
case "clear-range": {
|
|
383
|
-
const sheetIndex = resolveSheetIndex(workbook, op.sheet);
|
|
384
|
-
const r = parseRange(op.range);
|
|
385
|
-
return clearRangeCommand(workbook, engine.get(), sheetIndex, r.startRow, r.startCol, r.endRow, r.endCol);
|
|
386
|
-
}
|
|
387
|
-
case "merge": {
|
|
388
|
-
const sheetIndex = resolveSheetIndex(workbook, op.sheet);
|
|
389
|
-
const r = parseRange(op.range);
|
|
390
|
-
return mergeCellsCommand(workbook, sheetIndex, r.startRow, r.startCol, r.endRow, r.endCol);
|
|
391
|
-
}
|
|
392
|
-
case "unmerge": {
|
|
393
|
-
const sheetIndex = resolveSheetIndex(workbook, op.sheet);
|
|
394
|
-
const r = parseRange(op.range);
|
|
395
|
-
const mergeRef = `${rowColToRef(r.startRow, r.startCol)}:${rowColToRef(r.endRow, r.endCol)}`;
|
|
396
|
-
return unmergeCellsCommand(workbook, sheetIndex, mergeRef);
|
|
397
|
-
}
|
|
398
|
-
case "add-sheet":
|
|
399
|
-
return addSheetCommand(workbook, op.name);
|
|
400
|
-
case "delete-sheet":
|
|
401
|
-
return deleteSheetCommand(workbook, resolveSheetIndex(workbook, op.name));
|
|
402
|
-
case "rename-sheet":
|
|
403
|
-
return renameSheetCommand(workbook, resolveSheetIndex(workbook, op.old), op.new);
|
|
404
|
-
case "insert-rows":
|
|
405
|
-
return insertRowsCommand(workbook, resolveSheetIndex(workbook, op.sheet), op.at, op.count);
|
|
406
|
-
case "delete-rows":
|
|
407
|
-
return deleteRowsCommand(workbook, resolveSheetIndex(workbook, op.sheet), op.at, op.count);
|
|
408
|
-
case "insert-cols":
|
|
409
|
-
return insertColsCommand(workbook, resolveSheetIndex(workbook, op.sheet), op.at, op.count);
|
|
410
|
-
case "delete-cols":
|
|
411
|
-
return deleteColsCommand(workbook, resolveSheetIndex(workbook, op.sheet), op.at, op.count);
|
|
412
|
-
case "set-style": {
|
|
413
|
-
const sheetIndex = resolveSheetIndex(workbook, op.sheet);
|
|
414
|
-
const r = parseRange(op.range);
|
|
415
|
-
return setRangeStyleCommand(workbook, sheetIndex, r.startRow, r.startCol, r.endRow, r.endCol, op.style);
|
|
416
|
-
}
|
|
417
|
-
default: {
|
|
418
|
-
const exhaustive = op;
|
|
419
|
-
void exhaustive;
|
|
420
|
-
fail(`[op ${index}] Unknown op: ${JSON.stringify(op)}`);
|
|
421
|
-
}
|
|
422
|
-
}
|
|
423
|
-
}
|
|
424
|
-
// =============================================================================
|
|
425
|
-
// Dispatch + help
|
|
426
|
-
// =============================================================================
|
|
427
|
-
export function printXlsxHelp() {
|
|
428
|
-
console.error(`Lotics xlsx commands — manipulate .xlsx files in place.
|
|
429
|
-
Uses Lotics' own xlsx engine; round-trips faithfully with the Lotics editor and templates.
|
|
430
|
-
|
|
431
|
-
lotics xlsx read <file> Dump file as JSON (sheets, cells, merges, freeze)
|
|
432
|
-
lotics xlsx write <file> '<json>' Create .xlsx from {"sheets":[{"name","cells":{"A1":...}}]}
|
|
433
|
-
lotics xlsx set-cell <file> <sheet>!<ref> '<v>' Set cell value (prefix '=' for formula)
|
|
434
|
-
lotics xlsx clear-range <file> <sheet>!<range> Clear all cells in a range
|
|
435
|
-
lotics xlsx merge <file> <sheet>!<range> Merge a range into one cell
|
|
436
|
-
lotics xlsx unmerge <file> <sheet>!<range> Unmerge a previously merged range
|
|
437
|
-
lotics xlsx add-sheet <file> <name> Append a new sheet
|
|
438
|
-
lotics xlsx delete-sheet <file> <name> Remove a sheet
|
|
439
|
-
lotics xlsx rename-sheet <file> <old> <new> Rename a sheet
|
|
440
|
-
lotics xlsx insert-rows <file> <sheet> <at> <n> Insert <n> rows at 1-based position
|
|
441
|
-
lotics xlsx delete-rows <file> <sheet> <at> <n> Delete <n> rows starting at <at>
|
|
442
|
-
lotics xlsx insert-cols <file> <sheet> <at> <n> Insert <n> columns at 1-based position
|
|
443
|
-
lotics xlsx delete-cols <file> <sheet> <at> <n> Delete <n> columns starting at <at>
|
|
444
|
-
lotics xlsx set-style <file> <sheet>!<range> '<style-json>'
|
|
445
|
-
Apply partial CellStyle to range (fontBold, backgroundColor, ...)
|
|
446
|
-
lotics xlsx batch <file> '<json-array-of-ops>' Apply many ops in one parse/export cycle
|
|
447
|
-
|
|
448
|
-
Edit ops mutate the file atomically (temp file + rename).`);
|
|
449
|
-
}
|
|
450
|
-
export async function runXlsxCommand(subcommand, toolArgs, restArgs) {
|
|
451
|
-
switch (subcommand) {
|
|
452
|
-
case "read":
|
|
453
|
-
return xlsxRead(toolArgs);
|
|
454
|
-
case "write":
|
|
455
|
-
return xlsxWrite(toolArgs, restArgs[0]);
|
|
456
|
-
case "set-cell":
|
|
457
|
-
return xlsxSetCell(toolArgs, restArgs);
|
|
458
|
-
case "clear-range":
|
|
459
|
-
return xlsxClearRange(toolArgs, restArgs);
|
|
460
|
-
case "merge":
|
|
461
|
-
return xlsxMerge(toolArgs, restArgs);
|
|
462
|
-
case "unmerge":
|
|
463
|
-
return xlsxUnmerge(toolArgs, restArgs);
|
|
464
|
-
case "add-sheet":
|
|
465
|
-
return xlsxAddSheet(toolArgs, restArgs);
|
|
466
|
-
case "delete-sheet":
|
|
467
|
-
return xlsxDeleteSheet(toolArgs, restArgs);
|
|
468
|
-
case "rename-sheet":
|
|
469
|
-
return xlsxRenameSheet(toolArgs, restArgs);
|
|
470
|
-
case "insert-rows":
|
|
471
|
-
return xlsxInsertRows(toolArgs, restArgs);
|
|
472
|
-
case "delete-rows":
|
|
473
|
-
return xlsxDeleteRows(toolArgs, restArgs);
|
|
474
|
-
case "insert-cols":
|
|
475
|
-
return xlsxInsertCols(toolArgs, restArgs);
|
|
476
|
-
case "delete-cols":
|
|
477
|
-
return xlsxDeleteCols(toolArgs, restArgs);
|
|
478
|
-
case "set-style":
|
|
479
|
-
return xlsxSetStyle(toolArgs, restArgs);
|
|
480
|
-
case "batch":
|
|
481
|
-
return xlsxBatch(toolArgs, restArgs[0]);
|
|
482
|
-
default: {
|
|
483
|
-
if (subcommand)
|
|
484
|
-
console.error(`Unknown xlsx subcommand: ${subcommand}\n`);
|
|
485
|
-
printXlsxHelp();
|
|
486
|
-
process.exit(subcommand ? 1 : 0);
|
|
487
|
-
}
|
|
488
|
-
}
|
|
489
|
-
}
|
package/dist/xlsx.test.d.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|