@rex0220/print-craft-authoring-tools 0.1.0
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/LICENSE +27 -0
- package/README.md +54 -0
- package/SECURITY.md +43 -0
- package/THIRD_PARTY_NOTICES.md +45 -0
- package/dist/cli.mjs +3377 -0
- package/package.json +50 -0
- package/vendor/moment-with-locales.min.js +1 -0
- package/vendor/moment.json +6 -0
package/dist/cli.mjs
ADDED
|
@@ -0,0 +1,3377 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/*! @rex0220/print-craft-authoring-tools 0.1.0 (c) rex0220. MIT License. 計算式エンジンと印刷屋のコードは利用者の印刷屋プラグインの zip から読む(このファイルには含まれない)。commit bce9598. */
|
|
3
|
+
|
|
4
|
+
// src/cli.ts
|
|
5
|
+
import { existsSync as existsSync7, mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "node:fs";
|
|
6
|
+
import path10 from "node:path";
|
|
7
|
+
|
|
8
|
+
// src/env.ts
|
|
9
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
10
|
+
import path from "node:path";
|
|
11
|
+
|
|
12
|
+
// src/kintone-url.ts
|
|
13
|
+
var KINTONE_HOST_SUFFIXES = [".cybozu.com", ".kintone.com", ".cybozu.cn"];
|
|
14
|
+
var KintoneUrlError = class extends Error {
|
|
15
|
+
};
|
|
16
|
+
function normalizeKintoneBaseUrl(raw) {
|
|
17
|
+
const text = String(raw ?? "").trim();
|
|
18
|
+
let u;
|
|
19
|
+
try {
|
|
20
|
+
u = new URL(text);
|
|
21
|
+
} catch {
|
|
22
|
+
throw new KintoneUrlError(`kintone の URL として読めない: ${text.slice(0, 80)}`);
|
|
23
|
+
}
|
|
24
|
+
if (u.protocol !== "https:") throw new KintoneUrlError("kintone の URL は https で始める");
|
|
25
|
+
if (u.username || u.password) throw new KintoneUrlError("kintone の URL にユーザー名やパスワード(@)を入れない");
|
|
26
|
+
if (u.port) throw new KintoneUrlError("kintone の URL にポートを付けない");
|
|
27
|
+
if (u.search || u.hash) throw new KintoneUrlError("kintone の URL にクエリや # を付けない");
|
|
28
|
+
if (u.pathname !== "/" && u.pathname !== "") throw new KintoneUrlError("kintone の URL にパスを付けない(https://<サブドメイン>.cybozu.com だけ)");
|
|
29
|
+
const host = u.hostname.toLowerCase();
|
|
30
|
+
const wellFormed = /^[a-z0-9-]+(\.[a-z0-9-]+)+$/.test(host);
|
|
31
|
+
const suffix = KINTONE_HOST_SUFFIXES.find((s) => host.endsWith(s) && host.length > s.length);
|
|
32
|
+
if (!wellFormed || !suffix) {
|
|
33
|
+
throw new KintoneUrlError(`kintone のドメインではない: ${host}(使えるのは ${KINTONE_HOST_SUFFIXES.map((s) => `*${s}`).join(" / ")})`);
|
|
34
|
+
}
|
|
35
|
+
return `https://${host}`;
|
|
36
|
+
}
|
|
37
|
+
function isKintoneBaseUrl(raw) {
|
|
38
|
+
try {
|
|
39
|
+
normalizeKintoneBaseUrl(raw);
|
|
40
|
+
return true;
|
|
41
|
+
} catch {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// src/env.ts
|
|
47
|
+
var NAMES = {
|
|
48
|
+
baseUrl: ["KINTONE_BASE_URL", "KSQL_BASE_URL"],
|
|
49
|
+
token: ["KINTONE_API_TOKEN", "KSQL_TOKEN"],
|
|
50
|
+
username: ["KINTONE_USERNAME", "KSQL_USERNAME"],
|
|
51
|
+
password: ["KINTONE_PASSWORD", "KSQL_PASSWORD"],
|
|
52
|
+
pluginZip: ["PCRAFT_PLUGIN_ZIP"],
|
|
53
|
+
allowUnknown: ["PCRAFT_ALLOW_UNKNOWN_PLUGIN"]
|
|
54
|
+
};
|
|
55
|
+
function parseDotEnv(text) {
|
|
56
|
+
const out = {};
|
|
57
|
+
for (const raw of text.split(/\r?\n/)) {
|
|
58
|
+
const line = raw.trim();
|
|
59
|
+
if (!line || line.startsWith("#")) continue;
|
|
60
|
+
const m = line.match(/^(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)$/);
|
|
61
|
+
if (!m) continue;
|
|
62
|
+
let value = m[2].trim();
|
|
63
|
+
if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) value = value.slice(1, -1);
|
|
64
|
+
out[m[1]] = value;
|
|
65
|
+
}
|
|
66
|
+
return out;
|
|
67
|
+
}
|
|
68
|
+
var AuthError = class extends Error {
|
|
69
|
+
};
|
|
70
|
+
function picker(opt) {
|
|
71
|
+
const osEnv = opt.env ?? process.env;
|
|
72
|
+
const file = envFileOf(opt);
|
|
73
|
+
const fromFile = existsSync(file) ? parseDotEnv(readFileSync(file, "utf8")) : {};
|
|
74
|
+
return (names) => {
|
|
75
|
+
for (const src of [osEnv, fromFile]) {
|
|
76
|
+
for (const n of names) {
|
|
77
|
+
const v = src[n];
|
|
78
|
+
if (v && v.trim()) return v.trim();
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return void 0;
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
function envFileOf(opt) {
|
|
85
|
+
return opt.envFile ?? path.join(opt.cwd ?? process.cwd(), ".env");
|
|
86
|
+
}
|
|
87
|
+
function baseUrlFromEnv(opt = {}) {
|
|
88
|
+
const raw = picker(opt)(NAMES.baseUrl);
|
|
89
|
+
if (!raw) return void 0;
|
|
90
|
+
try {
|
|
91
|
+
return normalizeKintoneBaseUrl(raw);
|
|
92
|
+
} catch (e) {
|
|
93
|
+
throw new AuthError(`KINTONE_BASE_URL が不正: ${e instanceof KintoneUrlError ? e.message : String(e)}(https://<サブドメイン>.cybozu.com の形で書く)`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
function loadAuth(opt = {}) {
|
|
97
|
+
const pick = picker(opt);
|
|
98
|
+
const file = envFileOf(opt);
|
|
99
|
+
const baseUrl = baseUrlFromEnv(opt);
|
|
100
|
+
if (!baseUrl) throw new AuthError(`KINTONE_BASE_URL が無い(OS の環境変数か .env: ${file}。kintone 公式 MCP と同じ変数。dashboard の KSQL_BASE_URL でもよい)`);
|
|
101
|
+
const token = pick(NAMES.token);
|
|
102
|
+
const username = pick(NAMES.username);
|
|
103
|
+
const password = pick(NAMES.password);
|
|
104
|
+
if (!token && !(username && password)) throw new AuthError("KINTONE_API_TOKEN か、KINTONE_USERNAME と KINTONE_PASSWORD の両方が要る(OS の環境変数か .env)");
|
|
105
|
+
return { baseUrl, token, username: token ? void 0 : username, password: token ? void 0 : password };
|
|
106
|
+
}
|
|
107
|
+
function loadAuthForEnv(env, cwd = process.cwd()) {
|
|
108
|
+
const file = path.join(cwd, env.envFile);
|
|
109
|
+
if (!existsSync(file)) throw new AuthError(`環境「${env.name}」の認証のファイルが無い: ${env.envFile}(${env.envFile === ".env" ? ".env.example を写して作る" : "env/ に作る。.env と同じ書き方"})`);
|
|
110
|
+
const vars = parseDotEnv(readFileSync(file, "utf8"));
|
|
111
|
+
const pick = (names) => {
|
|
112
|
+
for (const n of names) if (vars[n] && vars[n].trim()) return vars[n].trim();
|
|
113
|
+
return void 0;
|
|
114
|
+
};
|
|
115
|
+
const fileBase = pick(NAMES.baseUrl);
|
|
116
|
+
if (fileBase) {
|
|
117
|
+
let normalized = "";
|
|
118
|
+
try {
|
|
119
|
+
normalized = normalizeKintoneBaseUrl(fileBase);
|
|
120
|
+
} catch {
|
|
121
|
+
normalized = "";
|
|
122
|
+
}
|
|
123
|
+
if (normalized !== env.baseUrl) throw new AuthError(`${env.envFile} の KINTONE_BASE_URL が environments.json の環境「${env.name}」の baseUrl(${env.baseUrl})と違う。取り違えを防ぐため止める(どちらかを直す)`);
|
|
124
|
+
}
|
|
125
|
+
const token = pick(NAMES.token);
|
|
126
|
+
const username = pick(NAMES.username);
|
|
127
|
+
const password = pick(NAMES.password);
|
|
128
|
+
if (!token && !(username && password)) throw new AuthError(`${env.envFile} に KINTONE_API_TOKEN か、KINTONE_USERNAME と KINTONE_PASSWORD の両方が要る(環境「${env.name}」。OS の環境変数は読まない)`);
|
|
129
|
+
return { baseUrl: env.baseUrl, token, username: token ? void 0 : username, password: token ? void 0 : password };
|
|
130
|
+
}
|
|
131
|
+
function pluginZipPath(opt = {}) {
|
|
132
|
+
const v = picker(opt)(NAMES.pluginZip);
|
|
133
|
+
if (!v) return void 0;
|
|
134
|
+
return path.resolve(path.dirname(envFileOf(opt)), v);
|
|
135
|
+
}
|
|
136
|
+
function allowUnknownPlugin(opt = {}) {
|
|
137
|
+
return picker(opt)(NAMES.allowUnknown) === "1";
|
|
138
|
+
}
|
|
139
|
+
function describeAuth(auth) {
|
|
140
|
+
return auth.token ? "API トークン" : "ログインユーザー";
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// src/workspace.ts
|
|
144
|
+
import { existsSync as existsSync2, readdirSync, readFileSync as readFileSync2, statSync } from "node:fs";
|
|
145
|
+
import path3 from "node:path";
|
|
146
|
+
|
|
147
|
+
// src/safe-path.ts
|
|
148
|
+
import { lstatSync, realpathSync } from "node:fs";
|
|
149
|
+
import path2 from "node:path";
|
|
150
|
+
var PathError = class extends Error {
|
|
151
|
+
};
|
|
152
|
+
var WRITE_ROOTS = {
|
|
153
|
+
fields: ["fields", "kintone"],
|
|
154
|
+
records: ["records", "kintone"],
|
|
155
|
+
settings: ["settings", "temp", "kintone"],
|
|
156
|
+
out: ["out", "kintone"],
|
|
157
|
+
kintone: ["kintone"]
|
|
158
|
+
};
|
|
159
|
+
var WINDOWS_RESERVED = /^(con|prn|aux|nul|com[1-9]|lpt[1-9])(\..*)?$/i;
|
|
160
|
+
function norm(p) {
|
|
161
|
+
const r = path2.resolve(p);
|
|
162
|
+
return process.platform === "win32" ? r.toLowerCase() : r;
|
|
163
|
+
}
|
|
164
|
+
function realResolve(target, cwd = process.cwd()) {
|
|
165
|
+
const abs = path2.resolve(cwd, target);
|
|
166
|
+
let dir = abs;
|
|
167
|
+
const rest = [];
|
|
168
|
+
for (; ; ) {
|
|
169
|
+
try {
|
|
170
|
+
lstatSync(dir);
|
|
171
|
+
break;
|
|
172
|
+
} catch (e) {
|
|
173
|
+
const code = e.code;
|
|
174
|
+
if (code === "ENOENT" || code === "ENOTDIR") {
|
|
175
|
+
const parent = path2.dirname(dir);
|
|
176
|
+
if (parent === dir) throw new PathError(`パスを解決できない: ${target}`);
|
|
177
|
+
rest.unshift(path2.basename(dir));
|
|
178
|
+
dir = parent;
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
throw new PathError(`パスを解決できない(アクセスできない: ${code ?? "不明"}): ${target}`);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
let real;
|
|
185
|
+
try {
|
|
186
|
+
real = realpathSync.native(dir);
|
|
187
|
+
} catch (e) {
|
|
188
|
+
throw new PathError(`パスを解決できない(リンク切れ・ループ・アクセスできない場所: ${e.code ?? "不明"}): ${target}`);
|
|
189
|
+
}
|
|
190
|
+
return rest.length ? path2.join(real, ...rest) : real;
|
|
191
|
+
}
|
|
192
|
+
function isInside(child, parent) {
|
|
193
|
+
const c = norm(child);
|
|
194
|
+
const p = norm(parent);
|
|
195
|
+
return c === p || c.startsWith(p.endsWith(path2.sep) ? p : p + path2.sep);
|
|
196
|
+
}
|
|
197
|
+
function resolveRead(target, cwd = process.cwd()) {
|
|
198
|
+
if (!target || typeof target !== "string") throw new PathError("ファイルのパスが要る");
|
|
199
|
+
const real = realResolve(target, cwd);
|
|
200
|
+
const cwdReal = realResolve(".", cwd);
|
|
201
|
+
if (!isInside(real, cwdReal)) throw new PathError(`読むファイルは作業フォルダーの中に置く: ${target}`);
|
|
202
|
+
const rel = path2.relative(cwdReal, real).replace(/\\/g, "/");
|
|
203
|
+
if (/^\.env(\.|$)/i.test(path2.basename(real)) || /^(node_modules|\.git)(\/|$)/i.test(rel)) throw new PathError(`このファイルは tools では読まない: ${target}`);
|
|
204
|
+
return real;
|
|
205
|
+
}
|
|
206
|
+
function resolveWrite(target, roots, cwd = process.cwd()) {
|
|
207
|
+
if (!target || typeof target !== "string") throw new PathError("書き込み先のパスが要る");
|
|
208
|
+
const real = realResolve(target, cwd);
|
|
209
|
+
const cwdReal = realResolve(".", cwd);
|
|
210
|
+
if (!isInside(real, cwdReal)) throw new PathError(`書き込み先は作業フォルダーの中: ${target}`);
|
|
211
|
+
const ok = roots.some((r) => {
|
|
212
|
+
const root = path2.join(cwdReal, r);
|
|
213
|
+
return isInside(real, root) && norm(real) !== norm(root);
|
|
214
|
+
});
|
|
215
|
+
if (!ok) throw new PathError(`書き込み先は ${roots.map((r) => `${r}/`).join(" か ")} の下: ${target}`);
|
|
216
|
+
const base = path2.basename(real);
|
|
217
|
+
if (WINDOWS_RESERVED.test(base) || /[. ]$/.test(base)) throw new PathError(`使えないファイル名: ${base}`);
|
|
218
|
+
return real;
|
|
219
|
+
}
|
|
220
|
+
function resolveWriteDir(target, roots, cwd = process.cwd()) {
|
|
221
|
+
if (!target || typeof target !== "string") throw new PathError("フォルダーのパスが要る");
|
|
222
|
+
const real = realResolve(target, cwd);
|
|
223
|
+
const cwdReal = realResolve(".", cwd);
|
|
224
|
+
if (!isInside(real, cwdReal)) throw new PathError(`出力フォルダーは作業フォルダーの中: ${target}`);
|
|
225
|
+
if (!roots.some((r) => isInside(real, path2.join(cwdReal, r)))) throw new PathError(`出力フォルダーは ${roots.map((r) => `${r}/`).join(" か ")}(またはその下): ${target}`);
|
|
226
|
+
return real;
|
|
227
|
+
}
|
|
228
|
+
function safeFileName(name, fallback) {
|
|
229
|
+
let s = name.replace(/[\\/:*?"<>|\u0000-\u001f\u007f]/g, "_").replace(/[. ]+$/g, "").trim();
|
|
230
|
+
if (!s || WINDOWS_RESERVED.test(s)) s = fallback;
|
|
231
|
+
return s.length > 120 ? s.slice(0, 120) : s;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// src/workspace.ts
|
|
235
|
+
var WORKSPACE_FILE = "environments.json";
|
|
236
|
+
var KINTONE_ROOT = "kintone";
|
|
237
|
+
var INBOX = "inbox";
|
|
238
|
+
var SNAPSHOT_PREFIX = "rex0220-print-craft";
|
|
239
|
+
var SNAPSHOT_RE = /^rex0220-print-craft-app(\d+)-(\d{8})-(\d{6})\.json$/;
|
|
240
|
+
var EDIT_RE = /^rex0220-print-craft-app(\d+)-(\d{8})-(\d{6})-edit\.json$/;
|
|
241
|
+
var ENV_NAME = /^[A-Za-z0-9_-]{1,32}$/;
|
|
242
|
+
var ENV_FILE = /^(\.env|env\/[A-Za-z0-9._-]{1,64}\.env)$/;
|
|
243
|
+
var MAX_BYTES = 256 * 1024;
|
|
244
|
+
var WorkspaceError = class extends Error {
|
|
245
|
+
};
|
|
246
|
+
var isEnvName = (s) => ENV_NAME.test(s);
|
|
247
|
+
function parseWorkspace(text, where = WORKSPACE_FILE) {
|
|
248
|
+
let raw;
|
|
249
|
+
try {
|
|
250
|
+
raw = JSON.parse(text);
|
|
251
|
+
} catch (e) {
|
|
252
|
+
throw new WorkspaceError(`${where} を JSON として読めない: ${e.message}`);
|
|
253
|
+
}
|
|
254
|
+
const obj = (v, at) => {
|
|
255
|
+
if (!v || typeof v !== "object" || Array.isArray(v)) throw new WorkspaceError(`${where}: ${at} はオブジェクト`);
|
|
256
|
+
return v;
|
|
257
|
+
};
|
|
258
|
+
const root = obj(raw, "最上位");
|
|
259
|
+
for (const k of Object.keys(root)) if (!["default", "environments", "apps"].includes(k)) throw new WorkspaceError(`${where}: 使えるキーは default / environments / apps: ${k}`);
|
|
260
|
+
const envsRaw = obj(root.environments, "environments");
|
|
261
|
+
const environments = {};
|
|
262
|
+
for (const [name, v] of Object.entries(envsRaw)) {
|
|
263
|
+
if (!isEnvName(name)) throw new WorkspaceError(`${where}: 環境の名前は英数字と _ - の 32 文字まで: ${name}`);
|
|
264
|
+
const e = obj(v, `environments.${name}`);
|
|
265
|
+
for (const k of Object.keys(e)) if (!["baseUrl", "envFile"].includes(k)) throw new WorkspaceError(`${where}: environments.${name} に使えるキーは baseUrl / envFile: ${k}`);
|
|
266
|
+
let baseUrl;
|
|
267
|
+
try {
|
|
268
|
+
baseUrl = normalizeKintoneBaseUrl(String(e.baseUrl ?? ""));
|
|
269
|
+
} catch (err) {
|
|
270
|
+
throw new WorkspaceError(`${where}: environments.${name}.baseUrl が不正: ${err instanceof KintoneUrlError ? err.message : String(err)}`);
|
|
271
|
+
}
|
|
272
|
+
const envFile = e.envFile === void 0 ? ".env" : String(e.envFile).replace(/\\/g, "/");
|
|
273
|
+
if (!ENV_FILE.test(envFile)) throw new WorkspaceError(`${where}: environments.${name}.envFile は .env か env/<名前>.env: ${envFile}`);
|
|
274
|
+
environments[name] = { name, baseUrl, host: new URL(baseUrl).hostname, envFile };
|
|
275
|
+
}
|
|
276
|
+
if (Object.keys(environments).length === 0) throw new WorkspaceError(`${where}: environments に環境が 1 つも無い`);
|
|
277
|
+
const defaultEnv = root.default === void 0 ? Object.keys(environments)[0] : String(root.default);
|
|
278
|
+
if (!environments[defaultEnv]) throw new WorkspaceError(`${where}: default の環境が environments に無い: ${defaultEnv}`);
|
|
279
|
+
const apps = [];
|
|
280
|
+
if (root.apps !== void 0) {
|
|
281
|
+
if (!Array.isArray(root.apps)) throw new WorkspaceError(`${where}: apps は配列`);
|
|
282
|
+
const names = /* @__PURE__ */ new Set();
|
|
283
|
+
for (const [i, v] of root.apps.entries()) {
|
|
284
|
+
const a = obj(v, `apps[${i}]`);
|
|
285
|
+
const name = typeof a.name === "string" ? a.name.trim() : "";
|
|
286
|
+
if (!name || name.length > 100 || /^\d+$/.test(name)) throw new WorkspaceError(`${where}: apps[${i}].name は 100 文字までの名前(数字だけは不可)`);
|
|
287
|
+
if (names.has(name)) throw new WorkspaceError(`${where}: apps の名前が重複: ${name}`);
|
|
288
|
+
names.add(name);
|
|
289
|
+
const ids = {};
|
|
290
|
+
for (const [k, n] of Object.entries(a)) {
|
|
291
|
+
if (k === "name") continue;
|
|
292
|
+
if (!environments[k]) throw new WorkspaceError(`${where}: apps[${i}] の ${k} は environments に無い環境`);
|
|
293
|
+
if (typeof n !== "number" || !Number.isInteger(n) || n <= 0) throw new WorkspaceError(`${where}: apps[${i}].${k} はアプリ番号(正の整数)`);
|
|
294
|
+
ids[k] = n;
|
|
295
|
+
}
|
|
296
|
+
apps.push({ name, ids });
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
return { defaultEnv, environments, apps };
|
|
300
|
+
}
|
|
301
|
+
function loadWorkspace(cwd = process.cwd()) {
|
|
302
|
+
const file = path3.join(cwd, WORKSPACE_FILE);
|
|
303
|
+
if (!existsSync2(file)) return null;
|
|
304
|
+
const size = statSync(file).size;
|
|
305
|
+
if (size > MAX_BYTES) throw new WorkspaceError(`${WORKSPACE_FILE} が大きすぎる(${size} バイト)`);
|
|
306
|
+
return parseWorkspace(readFileSync2(file, "utf8"));
|
|
307
|
+
}
|
|
308
|
+
function pickEnv(ws, name) {
|
|
309
|
+
const n = name ?? ws.defaultEnv;
|
|
310
|
+
const e = ws.environments[n];
|
|
311
|
+
if (!e) throw new WorkspaceError(`環境「${n}」は ${WORKSPACE_FILE} に無い(${Object.keys(ws.environments).join(", ")})`);
|
|
312
|
+
return e;
|
|
313
|
+
}
|
|
314
|
+
function resolveApp(ws, env, app) {
|
|
315
|
+
if (/^\d+$/.test(app)) {
|
|
316
|
+
const appId2 = Number(app);
|
|
317
|
+
const def2 = ws.apps.find((a) => a.ids[env.name] === appId2);
|
|
318
|
+
return { appId: appId2, ...def2 ? { name: def2.name } : {} };
|
|
319
|
+
}
|
|
320
|
+
const def = ws.apps.find((a) => a.name === app);
|
|
321
|
+
if (!def) throw new WorkspaceError(`アプリ「${app}」は ${WORKSPACE_FILE} の apps に無い(番号で書くか、apps に足す)`);
|
|
322
|
+
const appId = def.ids[env.name];
|
|
323
|
+
if (!appId) throw new WorkspaceError(`アプリ「${app}」の環境「${env.name}」の番号が ${WORKSPACE_FILE} に無い`);
|
|
324
|
+
return { appId, name: def.name };
|
|
325
|
+
}
|
|
326
|
+
function folderNameOf(appId, appName) {
|
|
327
|
+
const name = safeFileName(appName, "");
|
|
328
|
+
return name ? `${appId}-${name}` : String(appId);
|
|
329
|
+
}
|
|
330
|
+
function hostDirOf(cwd, env) {
|
|
331
|
+
return path3.join(cwd, KINTONE_ROOT, env.host);
|
|
332
|
+
}
|
|
333
|
+
function findAppDir(cwd, env, appId) {
|
|
334
|
+
const hostDir = hostDirOf(cwd, env);
|
|
335
|
+
if (!existsSync2(hostDir)) return null;
|
|
336
|
+
const hits = readdirSync(hostDir, { withFileTypes: true }).filter((d) => d.isDirectory() && (d.name === String(appId) || d.name.startsWith(`${appId}-`))).map((d) => d.name);
|
|
337
|
+
if (hits.length > 1) throw new WorkspaceError(`アプリ ${appId} のフォルダーが 2 つある: ${hits.join(", ")}(kintone/${env.host}/ の下を 1 つにする)`);
|
|
338
|
+
return hits.length ? path3.join(hostDir, hits[0]) : null;
|
|
339
|
+
}
|
|
340
|
+
function appDirFor(cwd, env, appId, appName) {
|
|
341
|
+
return findAppDir(cwd, env, appId) ?? path3.join(hostDirOf(cwd, env), folderNameOf(appId, appName));
|
|
342
|
+
}
|
|
343
|
+
function appFolderOfFile(cwd, file) {
|
|
344
|
+
const rel = path3.relative(path3.join(cwd, KINTONE_ROOT), path3.resolve(cwd, file)).replace(/\\/g, "/");
|
|
345
|
+
if (!rel || rel.startsWith("..")) return null;
|
|
346
|
+
const [host, folder, ...rest] = rel.split("/");
|
|
347
|
+
if (!host || !folder || rest.length === 0) return null;
|
|
348
|
+
const m = folder.match(/^(\d+)(?:-|$)/);
|
|
349
|
+
if (!m) return null;
|
|
350
|
+
return { dir: path3.join(cwd, KINTONE_ROOT, host, folder), host, appId: Number(m[1]) };
|
|
351
|
+
}
|
|
352
|
+
var pad = (n) => String(n).padStart(2, "0");
|
|
353
|
+
function snapshotNameOf(appId, now = /* @__PURE__ */ new Date()) {
|
|
354
|
+
return `${SNAPSHOT_PREFIX}-app${appId}-${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}-${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}.json`;
|
|
355
|
+
}
|
|
356
|
+
function editNameOf(snapshotFile) {
|
|
357
|
+
return snapshotFile.replace(/\.json$/, "-edit.json");
|
|
358
|
+
}
|
|
359
|
+
var stampOf = (name) => {
|
|
360
|
+
const m = name.match(/-(\d{8})-(\d{6})(?:-edit)?\.json$/);
|
|
361
|
+
return m ? `${m[1]}${m[2]}` : "";
|
|
362
|
+
};
|
|
363
|
+
var newestFirst = (a, b) => stampOf(b).localeCompare(stampOf(a)) || b.localeCompare(a);
|
|
364
|
+
function listAppFolder(dir) {
|
|
365
|
+
const files2 = existsSync2(dir) ? readdirSync(dir, { withFileTypes: true }) : [];
|
|
366
|
+
const names = files2.filter((d) => d.isFile() && d.name.endsWith(".json")).map((d) => d.name);
|
|
367
|
+
const sub = (name, filter) => {
|
|
368
|
+
const p = path3.join(dir, name);
|
|
369
|
+
return existsSync2(p) ? readdirSync(p).filter(filter).sort() : [];
|
|
370
|
+
};
|
|
371
|
+
return {
|
|
372
|
+
dir,
|
|
373
|
+
hasFields: names.includes("fields.json"),
|
|
374
|
+
snapshots: names.filter((n) => SNAPSHOT_RE.test(n)).sort(newestFirst),
|
|
375
|
+
edits: names.filter((n) => EDIT_RE.test(n)).sort(newestFirst),
|
|
376
|
+
reports: names.filter((n) => n !== "fields.json" && !SNAPSHOT_RE.test(n) && !EDIT_RE.test(n)).sort(),
|
|
377
|
+
records: sub("records", (n) => n.endsWith(".json")),
|
|
378
|
+
outs: sub("out", (n) => n.endsWith(".html")).length
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
function envsOfHost(ws, host) {
|
|
382
|
+
return Object.values(ws.environments).filter((e) => e.host === host);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// src/commands/take.ts
|
|
386
|
+
import { existsSync as existsSync4, readdirSync as readdirSync2, readFileSync as readFileSync5, unlinkSync, writeFileSync, mkdirSync } from "node:fs";
|
|
387
|
+
import path6 from "node:path";
|
|
388
|
+
|
|
389
|
+
// src/commands/normalize.ts
|
|
390
|
+
import { readFileSync as readFileSync4, statSync as statSync3 } from "node:fs";
|
|
391
|
+
import path5 from "node:path";
|
|
392
|
+
|
|
393
|
+
// src/normalize/findings.ts
|
|
394
|
+
var Findings = class {
|
|
395
|
+
items = [];
|
|
396
|
+
add(level, rule, where, message) {
|
|
397
|
+
this.items.push({ level, rule, where, message });
|
|
398
|
+
}
|
|
399
|
+
error(rule, where, message) {
|
|
400
|
+
this.add("error", rule, where, message);
|
|
401
|
+
}
|
|
402
|
+
warning(rule, where, message) {
|
|
403
|
+
this.add("warning", rule, where, message);
|
|
404
|
+
}
|
|
405
|
+
info(rule, where, message) {
|
|
406
|
+
this.add("info", rule, where, message);
|
|
407
|
+
}
|
|
408
|
+
count(level) {
|
|
409
|
+
return this.items.filter((f) => f.level === level).length;
|
|
410
|
+
}
|
|
411
|
+
get hasErrors() {
|
|
412
|
+
return this.count("error") > 0;
|
|
413
|
+
}
|
|
414
|
+
/** 画面に出す表(レベル → 場所 → 文言) */
|
|
415
|
+
format() {
|
|
416
|
+
const label = { error: "エラー", warning: "警告", info: "情報" };
|
|
417
|
+
const lines = [`エラー ${this.count("error")} / 警告 ${this.count("warning")} / 情報 ${this.count("info")}`];
|
|
418
|
+
for (const level of ["error", "warning", "info"]) {
|
|
419
|
+
for (const f of this.items.filter((x) => x.level === level)) lines.push(`[${label[level]}] ${f.where}: ${f.message}(${f.rule})`);
|
|
420
|
+
}
|
|
421
|
+
return lines.join("\n");
|
|
422
|
+
}
|
|
423
|
+
};
|
|
424
|
+
|
|
425
|
+
// src/normalize/model.ts
|
|
426
|
+
var PSEUDO_FIELDS = /* @__PURE__ */ new Set(["$id", "$revision", "$out", "$html", "$rseq"]);
|
|
427
|
+
var UOG_MARK = "$UGO$";
|
|
428
|
+
var EXEC_CONDITION_LABEL = "実行条件";
|
|
429
|
+
var clone = (v) => JSON.parse(JSON.stringify(v));
|
|
430
|
+
function buildModel(file, api, trustedBaseUrl2) {
|
|
431
|
+
const pp = { ...clone(file.properties) };
|
|
432
|
+
pp["$id"] = { type: "RECORD_NUMBER", code: "$id", label: "$id" };
|
|
433
|
+
pp["$revision"] = { type: "__REVISION__", code: "$revision", label: "$revision" };
|
|
434
|
+
pp["$html"] = { type: "MULTI_LINE_TEXT", code: "$html", label: "$html" };
|
|
435
|
+
const crec = api.createCheckRecord(pp);
|
|
436
|
+
const ppRun = api.expandFields(clone(file.properties), EXEC_CONDITION_LABEL);
|
|
437
|
+
const calcCandidates = api.createFieldsInfo(pp, clone(file.layout)).filter((c) => !c.ptcode && c.type !== "SUBTABLE");
|
|
438
|
+
const calcByCode = /* @__PURE__ */ new Map();
|
|
439
|
+
for (const c of calcCandidates) calcByCode.set(c.fieldcode, c);
|
|
440
|
+
const baseUrl = trustedBaseUrl2 ?? "";
|
|
441
|
+
const fieldsBaseUrl = typeof file.baseUrl === "string" && isKintoneBaseUrl(file.baseUrl) ? normalizeKintoneBaseUrl(file.baseUrl) : "";
|
|
442
|
+
return { api, file, baseUrl, fieldsBaseUrl, appId: file.appId, pp, crec, ppRun, calcCandidates, calcByCode };
|
|
443
|
+
}
|
|
444
|
+
function fieldExists(model, code) {
|
|
445
|
+
if (PSEUDO_FIELDS.has(code)) return true;
|
|
446
|
+
const p = model.pp[code];
|
|
447
|
+
if (!p) return false;
|
|
448
|
+
if (p.ptcode) return !!model.pp[p.ptcode];
|
|
449
|
+
return true;
|
|
450
|
+
}
|
|
451
|
+
function calcIneligibleReason(model, code) {
|
|
452
|
+
if (model.calcByCode.has(code)) return null;
|
|
453
|
+
const p = model.pp[code];
|
|
454
|
+
if (!p) return "fields に無い";
|
|
455
|
+
if (p.ptcode) return `テーブル ${p.ptcode} の中`;
|
|
456
|
+
if (!model.api.isTargetType(p.type)) return `更新できない型(${p.type})`;
|
|
457
|
+
if (p.expression) return "計算項目";
|
|
458
|
+
if (p.copyfield) return "ルックアップのコピー先";
|
|
459
|
+
return "レイアウトに無い";
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// src/normalize/derive.ts
|
|
463
|
+
var ENVELOPE_KEYS = ["date", "pluginName", "pluginID", "PluginVersion", "appId", "appName"];
|
|
464
|
+
var SHELL_KEYS = ["pluginProductEnv", "pluginLastUpdate", "pluginUpdater", "startDate", "name", "version", "ldate"];
|
|
465
|
+
var META_CALC_KEYS = ["row_type", "type", "lookup", "ptcode", "fieldlabel", "required"];
|
|
466
|
+
function evaluateFormula(engine, model, formulaSet) {
|
|
467
|
+
const formula = model.api.stripComments(formulaSet.replace(/\r\n?/g, "\n"));
|
|
468
|
+
if (!formula.trim()) return { formula, usedFields: {} };
|
|
469
|
+
const kf = engine.checker(model.pp, model.crec);
|
|
470
|
+
kf.usedFields({});
|
|
471
|
+
try {
|
|
472
|
+
kf.dq(formula);
|
|
473
|
+
} catch (e) {
|
|
474
|
+
return { formula, usedFields: {}, error: String(e?.message ?? e) };
|
|
475
|
+
}
|
|
476
|
+
return { formula, usedFields: kf.usedFields() };
|
|
477
|
+
}
|
|
478
|
+
function rowLabel(row, index) {
|
|
479
|
+
return row.menu || `ボタン ${index + 1}`;
|
|
480
|
+
}
|
|
481
|
+
function tagRowLabel(tag, index) {
|
|
482
|
+
const kind = index === 0 ? "ボタン表示条件" : index === 1 ? "ファイル名" : tag.desc || tag.fieldcode || "本文";
|
|
483
|
+
return `HTML 設定 ${index + 1} 行目 (${kind})`;
|
|
484
|
+
}
|
|
485
|
+
function bodyOf(envelope) {
|
|
486
|
+
const skip = /* @__PURE__ */ new Set([...ENVELOPE_KEYS, ...SHELL_KEYS]);
|
|
487
|
+
const out = {};
|
|
488
|
+
for (const k of Object.keys(envelope)) if (!skip.has(k)) out[k] = envelope[k];
|
|
489
|
+
return out;
|
|
490
|
+
}
|
|
491
|
+
function deriveBody(input, model, engine, f) {
|
|
492
|
+
const api = model.api;
|
|
493
|
+
const rows = api.buildMenuRows(input, "ja");
|
|
494
|
+
rows.forEach((row, i) => {
|
|
495
|
+
const label = rowLabel(row, i);
|
|
496
|
+
const tags = api.normalizeTagsInfo(row.tagsInfo);
|
|
497
|
+
if (tags) {
|
|
498
|
+
tags.fieldsInfo = tags.fieldsInfo.map((t, j) => {
|
|
499
|
+
const base = { ...t, id: j + 1, formulaSet: String(t.formulaSet ?? "").replace(/\r\n?/g, "\n") };
|
|
500
|
+
if (!t.state) return { ...base, formula: "", usedFields: {} };
|
|
501
|
+
const r = evaluateFormula(engine, model, base.formulaSet);
|
|
502
|
+
if (r.error) f.error("formula.syntax", `${label} / ${tagRowLabel(t, j)}`, `計算式を評価できない: ${r.error}`);
|
|
503
|
+
return { ...base, formula: r.formula, usedFields: r.usedFields };
|
|
504
|
+
});
|
|
505
|
+
row.tagsInfo = tags;
|
|
506
|
+
}
|
|
507
|
+
const given = row.calcInfo.fieldsInfo;
|
|
508
|
+
if (given.length === 0) {
|
|
509
|
+
row.calcInfo.usedFields = {};
|
|
510
|
+
row.calcInfo.flinkage = false;
|
|
511
|
+
return;
|
|
512
|
+
}
|
|
513
|
+
const outRow = given.find((x) => x.fieldcode === "$out");
|
|
514
|
+
const rebuilt = [];
|
|
515
|
+
rebuilt.push(
|
|
516
|
+
outRow ? { ...outRow, row_type: outRow.row_type || "更新条件", type: "BOOL", lookup: false, ptcode: "", fieldlabel: "", fieldcode: "$out", required: false } : { id: 1, state: false, row_type: "更新条件", type: "BOOL", lookup: false, ptcode: "", fieldlabel: "", fieldcode: "$out", required: false, remark: "", formulaSet: "1", formula: "1", usedFields: {} }
|
|
517
|
+
);
|
|
518
|
+
if (!outRow) f.info("calc.out", `${label} / 更新項目`, "先頭の $out(更新条件)が無いので既定(1 = 常に更新)を足した");
|
|
519
|
+
const seen = /* @__PURE__ */ new Set(["$out"]);
|
|
520
|
+
for (const x of given) {
|
|
521
|
+
if (x.fieldcode === "$out") continue;
|
|
522
|
+
if (seen.has(x.fieldcode)) {
|
|
523
|
+
f.error("calc.duplicate", `${label} / 更新項目 ${x.fieldcode}`, "同じ項目が 2 回ある");
|
|
524
|
+
continue;
|
|
525
|
+
}
|
|
526
|
+
seen.add(x.fieldcode);
|
|
527
|
+
const cand = model.calcByCode.get(x.fieldcode);
|
|
528
|
+
if (!cand) {
|
|
529
|
+
f.error("calc.ineligible", `${label} / 更新項目 ${x.fieldcode}`, `${x.fieldcode} は更新項目にできない: ${calcIneligibleReason(model, x.fieldcode)}`);
|
|
530
|
+
continue;
|
|
531
|
+
}
|
|
532
|
+
const diffs = META_CALC_KEYS.filter((k) => x[k] !== void 0 && x[k] !== "" && String(x[k]) !== String(cand[k] ?? ""));
|
|
533
|
+
if (diffs.length) f.info("calc.meta", `${label} / 更新項目 ${x.fieldcode}`, `fields の定義で上書き: ${diffs.map((k) => `${k} ${JSON.stringify(x[k])} → ${JSON.stringify(cand[k] ?? "")}`).join(", ")}`);
|
|
534
|
+
rebuilt.push({
|
|
535
|
+
id: 0,
|
|
536
|
+
state: !!x.state,
|
|
537
|
+
row_type: cand.row_type,
|
|
538
|
+
type: cand.type,
|
|
539
|
+
lookup: cand.lookup,
|
|
540
|
+
ptcode: cand.ptcode || "",
|
|
541
|
+
fieldlabel: cand.fieldlabel,
|
|
542
|
+
fieldcode: cand.fieldcode,
|
|
543
|
+
required: !!cand.required,
|
|
544
|
+
remark: String(x.remark ?? ""),
|
|
545
|
+
formulaSet: String(x.formulaSet ?? "").replace(/\r\n?/g, "\n"),
|
|
546
|
+
formula: "",
|
|
547
|
+
usedFields: {}
|
|
548
|
+
});
|
|
549
|
+
}
|
|
550
|
+
let used = {};
|
|
551
|
+
row.calcInfo.fieldsInfo = rebuilt.map((c, j) => {
|
|
552
|
+
const base = { ...c, id: j + 1 };
|
|
553
|
+
if (!c.state) return { ...base, formula: "", usedFields: {} };
|
|
554
|
+
const r = evaluateFormula(engine, model, base.formulaSet);
|
|
555
|
+
if (r.error) f.error("formula.syntax", `${label} / 更新項目 ${j + 1} 行目 (${c.fieldcode})`, `計算式を評価できない: ${r.error}`);
|
|
556
|
+
used = { ...used, ...r.usedFields };
|
|
557
|
+
return { ...base, formula: r.formula, usedFields: r.usedFields };
|
|
558
|
+
});
|
|
559
|
+
row.calcInfo.usedFields = used;
|
|
560
|
+
row.calcInfo.flinkage = false;
|
|
561
|
+
});
|
|
562
|
+
const saved = api.toSavedRows(rows);
|
|
563
|
+
const usage = api.computeUsage(rows, model.pp);
|
|
564
|
+
const body = {
|
|
565
|
+
pluginEnable: input.pluginEnable === void 0 ? true : !!input.pluginEnable,
|
|
566
|
+
menuInfo: input.menuInfo && typeof input.menuInfo === "object" ? input.menuInfo : api.defaultMenuInfo("ja"),
|
|
567
|
+
// ゲストの行番号は tools が振る(docs/設定ファイル仕様.md の guestsInfo[].id)
|
|
568
|
+
guestsInfo: Array.isArray(input.guestsInfo) ? input.guestsInfo.map((g, i) => g && typeof g === "object" && !Array.isArray(g) ? { ...g, id: i + 1 } : g) : [],
|
|
569
|
+
pluginComment: String(input.pluginComment ?? ""),
|
|
570
|
+
pluginDescription: String(input.pluginDescription ?? ""),
|
|
571
|
+
commonCssEnable: input.commonCssEnable === void 0 ? true : !!input.commonCssEnable,
|
|
572
|
+
// 共通 CSS は書いたときだけ出す。印刷屋 Ver.6 のアップロードの一部置換・追加は、ファイルに cssInfo があれば共通 CSS を丸ごと置き換えるかを
|
|
573
|
+
// 利用者が選ぶ(既定は置き換えない。Takashi 2026-10-05)。既定の 4 行を足しても意味がなく、置き換えを選ぶとアプリの共通 CSS が既定に戻る。無ければ印刷屋は既定の 4 行を使う
|
|
574
|
+
...Array.isArray(input.cssInfo) ? { cssInfo: api.normalizeCssRows(input.cssInfo) } : {},
|
|
575
|
+
...input.fontInfo && typeof input.fontInfo === "object" ? { fontInfo: input.fontInfo } : {},
|
|
576
|
+
// 外部参照(Ver.6 の共通の設定。設定画面は常に明示して保存する)。キーが無ければ印刷屋は Ver.5 の設定として「許可」で動く(Takashi 2026-10-04)ので
|
|
577
|
+
// "allow" を明示する(checks.ts が利用者の承認を求める)。"block" / "allow" 以外はそのまま残して checks.ts がエラーにする
|
|
578
|
+
externalRefs: input.externalRefs === void 0 ? "allow" : input.externalRefs,
|
|
579
|
+
pluginInfos: saved,
|
|
580
|
+
usedFields: usage.usedFields,
|
|
581
|
+
pluginUOG: usage.pluginUOG
|
|
582
|
+
};
|
|
583
|
+
for (const k of Object.keys(input)) if (!(k in body)) body[k] = input[k];
|
|
584
|
+
if (input.externalRefs === void 0) f.info("externalRefs.legacy", "共通の設定", `外部参照(externalRefs)が無い(Ver.5 の設定か、書き忘れ)。印刷屋はキーが無い設定を「許可」(何も除かない)として動かすので "allow" を明示した。新しい設定なら "block" を書く`);
|
|
585
|
+
return body;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
// src/normalize/css-check.ts
|
|
589
|
+
var PLACEHOLDER_RE = /^#\{&(?:p|n|f\([^{}]*\)|q\([^{}]*\))\}$/s;
|
|
590
|
+
var STR_MARK = "";
|
|
591
|
+
var STR_TOKEN = /\u0001(\d+)\u0001/g;
|
|
592
|
+
function cleanUrl(raw) {
|
|
593
|
+
let v = String(raw ?? "").replace(/[\t\n\r]/g, "");
|
|
594
|
+
v = v.replace(/^[\u0000- ]+|[\u0000- ]+$/g, "");
|
|
595
|
+
if (v.startsWith('"') && v.endsWith('"') || v.startsWith("'") && v.endsWith("'")) v = v.slice(1, -1);
|
|
596
|
+
return v.replace(/^[\u0000- ]+|[\u0000- ]+$/g, "");
|
|
597
|
+
}
|
|
598
|
+
function classifyUrl(raw) {
|
|
599
|
+
const v = cleanUrl(raw);
|
|
600
|
+
if (v === "") return "relative";
|
|
601
|
+
if (v.startsWith("#{&")) return PLACEHOLDER_RE.test(v) ? "placeholder" : "bad";
|
|
602
|
+
if (v.startsWith("#")) return "fragment";
|
|
603
|
+
if (/[\u0000-\u001f\u007f]/.test(v)) return "bad";
|
|
604
|
+
const scheme = v.match(/^([a-zA-Z][a-zA-Z0-9+.-]*):/)?.[1]?.toLowerCase();
|
|
605
|
+
if (!scheme) return v.replace(/\\/g, "/").startsWith("//") ? "bad" : "relative";
|
|
606
|
+
if (scheme === "https") return "https";
|
|
607
|
+
if (scheme === "data") {
|
|
608
|
+
if (/^data:image\/svg\+xml[;,]/i.test(v)) return "data-svg";
|
|
609
|
+
return /^data:image\/[a-z0-9.+-]+[;,]/i.test(v) ? "data-image" : "bad";
|
|
610
|
+
}
|
|
611
|
+
return "bad";
|
|
612
|
+
}
|
|
613
|
+
function parseSrcsetUrls(value) {
|
|
614
|
+
const urls = [];
|
|
615
|
+
const n = value.length;
|
|
616
|
+
let i = 0;
|
|
617
|
+
while (i < n) {
|
|
618
|
+
while (i < n && /[\s,]/.test(value[i])) i++;
|
|
619
|
+
if (i >= n) break;
|
|
620
|
+
const start = i;
|
|
621
|
+
while (i < n && !/\s/.test(value[i])) i++;
|
|
622
|
+
let url = value.slice(start, i);
|
|
623
|
+
if (url.endsWith(",")) {
|
|
624
|
+
url = url.replace(/,+$/, "");
|
|
625
|
+
if (url) urls.push(url);
|
|
626
|
+
continue;
|
|
627
|
+
}
|
|
628
|
+
if (url) urls.push(url);
|
|
629
|
+
let depth = 0;
|
|
630
|
+
while (i < n) {
|
|
631
|
+
const c = value[i];
|
|
632
|
+
if (c === "(") depth++;
|
|
633
|
+
else if (c === ")") depth--;
|
|
634
|
+
else if (c === "," && depth <= 0) {
|
|
635
|
+
i++;
|
|
636
|
+
break;
|
|
637
|
+
}
|
|
638
|
+
i++;
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
return urls;
|
|
642
|
+
}
|
|
643
|
+
function tokenizeCss(css) {
|
|
644
|
+
const strings = [];
|
|
645
|
+
let code = "";
|
|
646
|
+
let i = 0;
|
|
647
|
+
const n = css.length;
|
|
648
|
+
const readEscape = () => {
|
|
649
|
+
const hex = css.slice(i + 1, i + 7).match(/^[0-9a-fA-F]{1,6}/);
|
|
650
|
+
if (hex) {
|
|
651
|
+
i += 1 + hex[0].length;
|
|
652
|
+
if (css.startsWith("\r\n", i)) i += 2;
|
|
653
|
+
else if (i < n && /[ \t\r\n\f]/.test(css[i])) i++;
|
|
654
|
+
const cp = parseInt(hex[0], 16);
|
|
655
|
+
return cp === 0 || cp > 1114111 || cp >= 55296 && cp <= 57343 ? "�" : String.fromCodePoint(cp);
|
|
656
|
+
}
|
|
657
|
+
const c = css[i + 1];
|
|
658
|
+
if (c === void 0) {
|
|
659
|
+
i += 1;
|
|
660
|
+
return "�";
|
|
661
|
+
}
|
|
662
|
+
if (c === "\r" && css[i + 2] === "\n") {
|
|
663
|
+
i += 3;
|
|
664
|
+
return "";
|
|
665
|
+
}
|
|
666
|
+
if (c === "\n" || c === "\r" || c === "\f") {
|
|
667
|
+
i += 2;
|
|
668
|
+
return "";
|
|
669
|
+
}
|
|
670
|
+
i += 2;
|
|
671
|
+
return c;
|
|
672
|
+
};
|
|
673
|
+
while (i < n) {
|
|
674
|
+
const c = css[i];
|
|
675
|
+
if (c === "/" && css[i + 1] === "*") {
|
|
676
|
+
const end = css.indexOf("*/", i + 2);
|
|
677
|
+
i = end < 0 ? n : end + 2;
|
|
678
|
+
code += " ";
|
|
679
|
+
continue;
|
|
680
|
+
}
|
|
681
|
+
if (c === '"' || c === "'") {
|
|
682
|
+
const q = c;
|
|
683
|
+
i++;
|
|
684
|
+
let s = "";
|
|
685
|
+
while (i < n) {
|
|
686
|
+
const d = css[i];
|
|
687
|
+
if (d === "\\") {
|
|
688
|
+
s += readEscape();
|
|
689
|
+
continue;
|
|
690
|
+
}
|
|
691
|
+
if (d === q) {
|
|
692
|
+
i++;
|
|
693
|
+
break;
|
|
694
|
+
}
|
|
695
|
+
if (d === "\n" || d === "\r" || d === "\f") break;
|
|
696
|
+
s += d;
|
|
697
|
+
i++;
|
|
698
|
+
}
|
|
699
|
+
strings.push(s);
|
|
700
|
+
code += `${STR_MARK}${strings.length - 1}${STR_MARK}`;
|
|
701
|
+
continue;
|
|
702
|
+
}
|
|
703
|
+
if (c === "\\") {
|
|
704
|
+
code += readEscape();
|
|
705
|
+
continue;
|
|
706
|
+
}
|
|
707
|
+
code += c;
|
|
708
|
+
i++;
|
|
709
|
+
}
|
|
710
|
+
return { code, strings };
|
|
711
|
+
}
|
|
712
|
+
function extractUrls(css) {
|
|
713
|
+
const out = [];
|
|
714
|
+
const t = tokenizeCss(css);
|
|
715
|
+
const code = t.code;
|
|
716
|
+
const str = (token) => t.strings[Number(token.slice(1, -1))] ?? "";
|
|
717
|
+
const re = /\burl\(\s*/gi;
|
|
718
|
+
let m;
|
|
719
|
+
while (m = re.exec(code)) {
|
|
720
|
+
const i = m.index + m[0].length;
|
|
721
|
+
const ph = code.slice(i).match(/^\u0001\d+\u0001/);
|
|
722
|
+
let value = "";
|
|
723
|
+
if (ph) value = str(ph[0]);
|
|
724
|
+
else if (code.startsWith("#{&", i)) {
|
|
725
|
+
const end = code.indexOf(")}", i);
|
|
726
|
+
value = code.slice(i, end < 0 ? code.length : end + 2);
|
|
727
|
+
} else {
|
|
728
|
+
const end = code.indexOf(")", i);
|
|
729
|
+
value = code.slice(i, end < 0 ? code.length : end);
|
|
730
|
+
}
|
|
731
|
+
out.push({ url: value.trim(), via: "url()" });
|
|
732
|
+
}
|
|
733
|
+
const fns = /\b(?:-webkit-)?(image-set|image|src)\(((?:[^()]|\([^()]*\))*)\)/gi;
|
|
734
|
+
while (m = fns.exec(code)) {
|
|
735
|
+
for (const s of m[2].matchAll(STR_TOKEN)) out.push({ url: str(s[0]).trim(), via: m[1].toLowerCase() });
|
|
736
|
+
}
|
|
737
|
+
const imp = /@import\s+(?:url\([^)]*\)|(\u0001\d+\u0001))/gi;
|
|
738
|
+
while (m = imp.exec(code)) if (m[1] !== void 0) out.push({ url: str(m[1]).trim(), via: "@import" });
|
|
739
|
+
return out;
|
|
740
|
+
}
|
|
741
|
+
function checkCss(css) {
|
|
742
|
+
const errors = [];
|
|
743
|
+
const externals = [];
|
|
744
|
+
const { code } = tokenizeCss(css);
|
|
745
|
+
if (/@import\b/i.test(code)) errors.push("@import は使えない(外部の CSS を読む)");
|
|
746
|
+
if (/\bexpression\s*\(/i.test(code)) errors.push("expression( は使えない");
|
|
747
|
+
if (/\bbehavior\s*:/i.test(code)) errors.push("behavior: は使えない");
|
|
748
|
+
if (/-moz-binding\s*:/i.test(code)) errors.push("-moz-binding: は使えない");
|
|
749
|
+
for (const u of extractUrls(css)) {
|
|
750
|
+
const kind = classifyUrl(u.url);
|
|
751
|
+
if (kind === "https") externals.push({ url: cleanUrl(u.url), via: u.via });
|
|
752
|
+
else if (kind === "bad") errors.push(`${u.via} の URL が使えない形: ${u.url.slice(0, 80)}(https:、data:image/、#{&f(…)} だけ)`);
|
|
753
|
+
}
|
|
754
|
+
return { errors, externals };
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
// src/normalize/html-check.ts
|
|
758
|
+
var ALLOWED_TAGS = /* @__PURE__ */ new Set([
|
|
759
|
+
"div",
|
|
760
|
+
"span",
|
|
761
|
+
"p",
|
|
762
|
+
"br",
|
|
763
|
+
"hr",
|
|
764
|
+
"img",
|
|
765
|
+
"table",
|
|
766
|
+
"thead",
|
|
767
|
+
"tbody",
|
|
768
|
+
"tfoot",
|
|
769
|
+
"tr",
|
|
770
|
+
"th",
|
|
771
|
+
"td",
|
|
772
|
+
"caption",
|
|
773
|
+
"colgroup",
|
|
774
|
+
"col",
|
|
775
|
+
"ul",
|
|
776
|
+
"ol",
|
|
777
|
+
"li",
|
|
778
|
+
"dl",
|
|
779
|
+
"dt",
|
|
780
|
+
"dd",
|
|
781
|
+
"h1",
|
|
782
|
+
"h2",
|
|
783
|
+
"h3",
|
|
784
|
+
"h4",
|
|
785
|
+
"h5",
|
|
786
|
+
"h6",
|
|
787
|
+
"strong",
|
|
788
|
+
"b",
|
|
789
|
+
"em",
|
|
790
|
+
"i",
|
|
791
|
+
"u",
|
|
792
|
+
"s",
|
|
793
|
+
"small",
|
|
794
|
+
"sub",
|
|
795
|
+
"sup",
|
|
796
|
+
"mark",
|
|
797
|
+
"del",
|
|
798
|
+
"ins",
|
|
799
|
+
"a",
|
|
800
|
+
"section",
|
|
801
|
+
"article",
|
|
802
|
+
"header",
|
|
803
|
+
"footer",
|
|
804
|
+
"main",
|
|
805
|
+
"nav",
|
|
806
|
+
"aside",
|
|
807
|
+
"figure",
|
|
808
|
+
"figcaption",
|
|
809
|
+
"pre",
|
|
810
|
+
"code",
|
|
811
|
+
"blockquote",
|
|
812
|
+
"q",
|
|
813
|
+
"cite",
|
|
814
|
+
"abbr",
|
|
815
|
+
"time",
|
|
816
|
+
"address",
|
|
817
|
+
"label",
|
|
818
|
+
"wbr",
|
|
819
|
+
"iframe",
|
|
820
|
+
"style",
|
|
821
|
+
"ruby",
|
|
822
|
+
"rt",
|
|
823
|
+
"rp",
|
|
824
|
+
"bdi",
|
|
825
|
+
"bdo",
|
|
826
|
+
"big",
|
|
827
|
+
"center",
|
|
828
|
+
"font",
|
|
829
|
+
"tt",
|
|
830
|
+
"kbd",
|
|
831
|
+
"samp",
|
|
832
|
+
"var",
|
|
833
|
+
"dfn"
|
|
834
|
+
]);
|
|
835
|
+
var DENIED_REASON = {
|
|
836
|
+
script: "スクリプト",
|
|
837
|
+
object: "外部の内容を埋め込む",
|
|
838
|
+
embed: "外部の内容を埋め込む",
|
|
839
|
+
applet: "外部の内容を埋め込む",
|
|
840
|
+
link: "外部の CSS を読む",
|
|
841
|
+
base: "リンクの基準を変える",
|
|
842
|
+
meta: "文書の動きを変える(refresh など)",
|
|
843
|
+
noscript: "",
|
|
844
|
+
template: "",
|
|
845
|
+
slot: "",
|
|
846
|
+
portal: "",
|
|
847
|
+
form: "送信の経路",
|
|
848
|
+
input: "入力欄",
|
|
849
|
+
button: "入力欄",
|
|
850
|
+
select: "入力欄",
|
|
851
|
+
textarea: "入力欄",
|
|
852
|
+
option: "入力欄",
|
|
853
|
+
optgroup: "入力欄",
|
|
854
|
+
frame: "",
|
|
855
|
+
frameset: "",
|
|
856
|
+
svg: 'インラインの SVG は使わない(図は <img src="data:image/svg+xml,…"> か添付ファイルの #{&f(…)} で入れる)',
|
|
857
|
+
math: "MathML は使わない",
|
|
858
|
+
video: "動画は PDF にならない",
|
|
859
|
+
audio: "音声は PDF にならない",
|
|
860
|
+
source: "",
|
|
861
|
+
track: "",
|
|
862
|
+
picture: "<img src> だけ使う",
|
|
863
|
+
canvas: "",
|
|
864
|
+
map: "",
|
|
865
|
+
area: "",
|
|
866
|
+
dialog: "",
|
|
867
|
+
details: "",
|
|
868
|
+
summary: "",
|
|
869
|
+
marquee: "",
|
|
870
|
+
xmp: "",
|
|
871
|
+
plaintext: "",
|
|
872
|
+
listing: "",
|
|
873
|
+
html: "",
|
|
874
|
+
head: "",
|
|
875
|
+
body: "",
|
|
876
|
+
title: ""
|
|
877
|
+
};
|
|
878
|
+
var GLOBAL_ATTRS = /* @__PURE__ */ new Set(["class", "id", "style", "title", "lang", "dir", "hidden", "translate", "role", "align", "valign", "bgcolor", "border", "width", "height", "color", "nowrap", "char", "charoff"]);
|
|
879
|
+
var TAG_ATTRS = {
|
|
880
|
+
img: ["src", "srcset", "alt", "loading", "decoding", "sizes"],
|
|
881
|
+
a: ["href", "target", "rel", "name"],
|
|
882
|
+
td: ["colspan", "rowspan", "headers", "scope", "abbr"],
|
|
883
|
+
th: ["colspan", "rowspan", "headers", "scope", "abbr"],
|
|
884
|
+
col: ["span"],
|
|
885
|
+
colgroup: ["span"],
|
|
886
|
+
table: ["cellpadding", "cellspacing", "summary", "rules", "frame"],
|
|
887
|
+
ol: ["start", "reversed", "type"],
|
|
888
|
+
ul: ["type"],
|
|
889
|
+
li: ["value"],
|
|
890
|
+
time: ["datetime"],
|
|
891
|
+
q: ["cite"],
|
|
892
|
+
blockquote: ["cite"],
|
|
893
|
+
ins: ["cite", "datetime"],
|
|
894
|
+
del: ["cite", "datetime"],
|
|
895
|
+
iframe: ["src", "name", "frameborder", "scrolling"],
|
|
896
|
+
label: ["for"],
|
|
897
|
+
style: ["media", "type"],
|
|
898
|
+
font: ["face", "size"]
|
|
899
|
+
};
|
|
900
|
+
var URL_ATTRS = /* @__PURE__ */ new Set(["src", "srcset", "href", "cite"]);
|
|
901
|
+
function tagError(tag, where = "") {
|
|
902
|
+
const t = tag.toLowerCase();
|
|
903
|
+
if (ALLOWED_TAGS.has(t)) return null;
|
|
904
|
+
const reason = DENIED_REASON[t];
|
|
905
|
+
return `<${t}> は使えない(${where}${where ? "。" : ""}${reason ? reason : "許可の一覧に無い要素"})`;
|
|
906
|
+
}
|
|
907
|
+
function attrAllowed(tag, name) {
|
|
908
|
+
if (GLOBAL_ATTRS.has(name)) return true;
|
|
909
|
+
if (name.startsWith("data-") || name.startsWith("aria-")) return true;
|
|
910
|
+
if (tag === void 0) return Object.values(TAG_ATTRS).some((list) => list.includes(name));
|
|
911
|
+
return (TAG_ATTRS[tag] ?? []).includes(name);
|
|
912
|
+
}
|
|
913
|
+
var windowPromise = null;
|
|
914
|
+
async function dom() {
|
|
915
|
+
if (!windowPromise) {
|
|
916
|
+
windowPromise = (async () => {
|
|
917
|
+
const { Window } = await import("happy-dom");
|
|
918
|
+
return new Window({ url: "https://authoring.local/" });
|
|
919
|
+
})();
|
|
920
|
+
}
|
|
921
|
+
return windowPromise;
|
|
922
|
+
}
|
|
923
|
+
function sameOrigin(url, baseUrl) {
|
|
924
|
+
if (!baseUrl) return false;
|
|
925
|
+
try {
|
|
926
|
+
return new URL(url).origin === new URL(baseUrl).origin;
|
|
927
|
+
} catch {
|
|
928
|
+
return false;
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
async function checkHtml(html, opt = {}) {
|
|
932
|
+
const errors = [];
|
|
933
|
+
const warnings = [];
|
|
934
|
+
const externals = [];
|
|
935
|
+
if (!html.trim()) return { errors, warnings, externals };
|
|
936
|
+
const w = await dom();
|
|
937
|
+
const doc = new w.DOMParser().parseFromString(`<!doctype html><html><body>${html}</body></html>`, "text/html");
|
|
938
|
+
const root = doc.body ?? doc.documentElement;
|
|
939
|
+
if (!root) return { errors, warnings, externals };
|
|
940
|
+
const walk = (el, path11) => {
|
|
941
|
+
const tag = el.tagName.toLowerCase();
|
|
942
|
+
const here2 = `${path11}>${tag}`;
|
|
943
|
+
const denied = tagError(tag, here2);
|
|
944
|
+
if (denied) {
|
|
945
|
+
errors.push(denied);
|
|
946
|
+
return;
|
|
947
|
+
}
|
|
948
|
+
if (tag === "style") {
|
|
949
|
+
const r = checkCss(el.textContent ?? "");
|
|
950
|
+
for (const e of r.errors) errors.push(`<style>: ${e}`);
|
|
951
|
+
for (const u of r.externals) externals.push({ url: u.url, via: `<style> ${u.via}` });
|
|
952
|
+
}
|
|
953
|
+
for (const a of Array.from(el.attributes)) {
|
|
954
|
+
const name = a.name.toLowerCase();
|
|
955
|
+
const value = a.value ?? "";
|
|
956
|
+
if (name.startsWith("on")) {
|
|
957
|
+
errors.push(`${name} 属性(イベント)は使えない(${here2})`);
|
|
958
|
+
continue;
|
|
959
|
+
}
|
|
960
|
+
if (!attrAllowed(tag, name)) {
|
|
961
|
+
errors.push(`${name} 属性は使えない(${here2}。許可の一覧に無い属性)`);
|
|
962
|
+
continue;
|
|
963
|
+
}
|
|
964
|
+
if (value.includes("${")) {
|
|
965
|
+
errors.push(`属性値に \${式} を置けない(${here2}[${name}]。ESC_HTML は引用符を逃がさないので属性が壊れる。属性の値は固定にする)`);
|
|
966
|
+
continue;
|
|
967
|
+
}
|
|
968
|
+
if (name === "style") {
|
|
969
|
+
const r = checkCss(`x{${value}}`);
|
|
970
|
+
for (const e of r.errors) errors.push(`style 属性: ${e}(${here2})`);
|
|
971
|
+
for (const u of r.externals) externals.push({ url: u.url, via: `${here2}[style] ${u.via}` });
|
|
972
|
+
continue;
|
|
973
|
+
}
|
|
974
|
+
if (URL_ATTRS.has(name)) {
|
|
975
|
+
const urls = name === "srcset" ? parseSrcsetUrls(value) : [value];
|
|
976
|
+
for (const u of urls) {
|
|
977
|
+
const kind = classifyUrl(u);
|
|
978
|
+
const shown2 = cleanUrl(u).slice(0, 80);
|
|
979
|
+
if (kind === "bad") {
|
|
980
|
+
errors.push(`${name} の URL が使えない形: ${shown2}(${here2}。https:、data:image/、#{&f(…)}、相対だけ)`);
|
|
981
|
+
continue;
|
|
982
|
+
}
|
|
983
|
+
if (tag === "a" && (kind === "data-image" || kind === "data-svg")) {
|
|
984
|
+
errors.push(`href に data: は使えない(${here2})`);
|
|
985
|
+
continue;
|
|
986
|
+
}
|
|
987
|
+
if (tag === "iframe" && name === "src") {
|
|
988
|
+
if (kind !== "https" || !sameOrigin(cleanUrl(u), opt.baseUrl)) errors.push(`iframe の src は利用者の kintone(${opt.baseUrl ?? ".env の KINTONE_BASE_URL。未設定なので iframe は使えない"})だけ: ${shown2}(${here2})`);
|
|
989
|
+
continue;
|
|
990
|
+
}
|
|
991
|
+
if (kind === "https") externals.push({ url: cleanUrl(u), via: `${here2}[${name}]` });
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
for (const c of Array.from(el.children)) walk(c, here2);
|
|
996
|
+
};
|
|
997
|
+
for (const c of Array.from(root.children)) walk(c, "");
|
|
998
|
+
return { errors, warnings, externals };
|
|
999
|
+
}
|
|
1000
|
+
function expressionsOf(html) {
|
|
1001
|
+
return [...html.matchAll(/\$\{(.*?)\}/g)].map((m) => m[1].trim());
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
// src/normalize/policy.ts
|
|
1005
|
+
import { existsSync as existsSync3, readFileSync as readFileSync3, statSync as statSync2 } from "node:fs";
|
|
1006
|
+
import path4 from "node:path";
|
|
1007
|
+
var PolicyError = class extends Error {
|
|
1008
|
+
};
|
|
1009
|
+
var POLICY_FILE = path4.join("policy", "authoring-policy.json");
|
|
1010
|
+
var DEFAULT_ALLOWED_ORIGINS = ["https://fonts.googleapis.com", "https://fonts.gstatic.com"];
|
|
1011
|
+
var MAX_POLICY_BYTES = 1024 * 1024;
|
|
1012
|
+
var MAX_RULES = 200;
|
|
1013
|
+
var MAX_FILES = 100;
|
|
1014
|
+
function normalizeRel(p) {
|
|
1015
|
+
let s = String(p).replace(/\\/g, "/").replace(/\/{2,}/g, "/");
|
|
1016
|
+
while (s.startsWith("./")) s = s.slice(2);
|
|
1017
|
+
if (s.startsWith("/") || /^[a-zA-Z]:/.test(s) || s.split("/").includes("..")) throw new PolicyError(`files は作業フォルダーからの相対パスで書く(.. や絶対パスは使えない): ${p}`);
|
|
1018
|
+
return process.platform === "win32" ? s.toLowerCase() : s;
|
|
1019
|
+
}
|
|
1020
|
+
function fail(file, where, msg) {
|
|
1021
|
+
throw new PolicyError(`${file} の形が不正(${where}): ${msg}`);
|
|
1022
|
+
}
|
|
1023
|
+
function parsePolicy(text, file) {
|
|
1024
|
+
let raw;
|
|
1025
|
+
try {
|
|
1026
|
+
raw = JSON.parse(text);
|
|
1027
|
+
} catch (e) {
|
|
1028
|
+
fail(file, "$", `JSON として読めない: ${e.message}`);
|
|
1029
|
+
}
|
|
1030
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) fail(file, "$", "最上位はオブジェクト");
|
|
1031
|
+
const obj = raw;
|
|
1032
|
+
for (const k of Object.keys(obj)) if (k !== "allowExternal" && k !== "allowExternalRefs") fail(file, `$.${k}`, "使えるキーは allowExternal と allowExternalRefs だけ");
|
|
1033
|
+
let allowExternalRefs;
|
|
1034
|
+
if (obj.allowExternalRefs !== void 0) {
|
|
1035
|
+
const files2 = obj.allowExternalRefs;
|
|
1036
|
+
if (!Array.isArray(files2) || files2.some((f) => typeof f !== "string")) fail(file, "$.allowExternalRefs", "設定ファイルの相対パスの配列で書く");
|
|
1037
|
+
if (files2.length > MAX_FILES) fail(file, "$.allowExternalRefs", `${MAX_FILES} 件まで`);
|
|
1038
|
+
allowExternalRefs = files2.map((f) => {
|
|
1039
|
+
try {
|
|
1040
|
+
return normalizeRel(f);
|
|
1041
|
+
} catch (e) {
|
|
1042
|
+
return fail(file, "$.allowExternalRefs", e.message);
|
|
1043
|
+
}
|
|
1044
|
+
});
|
|
1045
|
+
}
|
|
1046
|
+
const list = obj.allowExternal ?? [];
|
|
1047
|
+
if (!Array.isArray(list)) fail(file, "$.allowExternal", "配列で書く");
|
|
1048
|
+
if (list.length > MAX_RULES) fail(file, "$.allowExternal", `${MAX_RULES} 件まで`);
|
|
1049
|
+
const rules = [];
|
|
1050
|
+
list.forEach((r, i) => {
|
|
1051
|
+
const at = `$.allowExternal[${i}]`;
|
|
1052
|
+
if (!r || typeof r !== "object" || Array.isArray(r)) fail(file, at, "オブジェクトで書く");
|
|
1053
|
+
const o = r;
|
|
1054
|
+
for (const k of Object.keys(o)) if (!["origin", "url", "files", "note"].includes(k)) fail(file, `${at}.${k}`, "使えるキーは origin / url / files / note");
|
|
1055
|
+
const rule = {};
|
|
1056
|
+
if (o.origin !== void 0) {
|
|
1057
|
+
if (typeof o.origin !== "string") fail(file, `${at}.origin`, "文字列で書く");
|
|
1058
|
+
let u;
|
|
1059
|
+
try {
|
|
1060
|
+
u = new URL(o.origin);
|
|
1061
|
+
} catch {
|
|
1062
|
+
fail(file, `${at}.origin`, `URL として読めない: ${o.origin}`);
|
|
1063
|
+
}
|
|
1064
|
+
if (u.protocol !== "https:" || u.username || u.password || u.origin !== o.origin.replace(/\/$/, "")) fail(file, `${at}.origin`, `https のオリジンだけを書く(例 https://cdn.example.com): ${o.origin}`);
|
|
1065
|
+
rule.origin = u.origin;
|
|
1066
|
+
}
|
|
1067
|
+
if (o.url !== void 0) {
|
|
1068
|
+
if (typeof o.url !== "string") fail(file, `${at}.url`, "文字列で書く");
|
|
1069
|
+
let u;
|
|
1070
|
+
try {
|
|
1071
|
+
u = new URL(o.url);
|
|
1072
|
+
} catch {
|
|
1073
|
+
fail(file, `${at}.url`, `URL として読めない: ${o.url}`);
|
|
1074
|
+
}
|
|
1075
|
+
if (u.protocol !== "https:" || u.username || u.password) fail(file, `${at}.url`, `https の完全な URL を書く: ${o.url}`);
|
|
1076
|
+
rule.url = u.href;
|
|
1077
|
+
}
|
|
1078
|
+
if (rule.origin === void 0 && rule.url === void 0) fail(file, at, "origin か url のどちらかが要る");
|
|
1079
|
+
if (o.files !== void 0) {
|
|
1080
|
+
if (!Array.isArray(o.files) || o.files.some((f) => typeof f !== "string")) fail(file, `${at}.files`, "文字列の配列で書く");
|
|
1081
|
+
if (o.files.length > MAX_FILES) fail(file, `${at}.files`, `${MAX_FILES} 件まで`);
|
|
1082
|
+
rule.files = o.files.map((f) => {
|
|
1083
|
+
try {
|
|
1084
|
+
return normalizeRel(f);
|
|
1085
|
+
} catch (e) {
|
|
1086
|
+
return fail(file, `${at}.files`, e.message);
|
|
1087
|
+
}
|
|
1088
|
+
});
|
|
1089
|
+
}
|
|
1090
|
+
if (o.note !== void 0) {
|
|
1091
|
+
if (typeof o.note !== "string") fail(file, `${at}.note`, "文字列で書く");
|
|
1092
|
+
rule.note = o.note;
|
|
1093
|
+
}
|
|
1094
|
+
rules.push(rule);
|
|
1095
|
+
});
|
|
1096
|
+
return { allowExternal: rules, ...allowExternalRefs ? { allowExternalRefs } : {}, file };
|
|
1097
|
+
}
|
|
1098
|
+
function isExternalRefsAllowed(policy, settingsFile) {
|
|
1099
|
+
if (!policy.allowExternalRefs?.length || !settingsFile) return false;
|
|
1100
|
+
try {
|
|
1101
|
+
return policy.allowExternalRefs.includes(normalizeRel(settingsFile));
|
|
1102
|
+
} catch {
|
|
1103
|
+
return false;
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
function loadPolicy(opt = {}) {
|
|
1107
|
+
const file = path4.join(opt.cwd ?? process.cwd(), POLICY_FILE);
|
|
1108
|
+
if (!existsSync3(file)) return { allowExternal: [] };
|
|
1109
|
+
if (statSync2(file).size > MAX_POLICY_BYTES) throw new PolicyError(`${file} が大きすぎる(${MAX_POLICY_BYTES} バイトまで)`);
|
|
1110
|
+
return parsePolicy(readFileSync3(file, "utf8"), file);
|
|
1111
|
+
}
|
|
1112
|
+
function originOf(url) {
|
|
1113
|
+
try {
|
|
1114
|
+
return new URL(url).origin;
|
|
1115
|
+
} catch {
|
|
1116
|
+
return null;
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
function hrefOf(url) {
|
|
1120
|
+
try {
|
|
1121
|
+
return new URL(url).href;
|
|
1122
|
+
} catch {
|
|
1123
|
+
return null;
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
function isAllowed(policy, url, settingsFile) {
|
|
1127
|
+
const u = cleanUrl(url);
|
|
1128
|
+
const origin = originOf(u);
|
|
1129
|
+
const href = hrefOf(u);
|
|
1130
|
+
if (origin && DEFAULT_ALLOWED_ORIGINS.includes(origin)) return true;
|
|
1131
|
+
let rel;
|
|
1132
|
+
if (settingsFile) {
|
|
1133
|
+
try {
|
|
1134
|
+
rel = normalizeRel(settingsFile);
|
|
1135
|
+
} catch {
|
|
1136
|
+
rel = void 0;
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
for (const r of policy.allowExternal) {
|
|
1140
|
+
const hit = r.url && href && r.url === href || r.origin && origin && r.origin === origin;
|
|
1141
|
+
if (!hit) continue;
|
|
1142
|
+
if (r.files && r.files.length) {
|
|
1143
|
+
if (!rel) continue;
|
|
1144
|
+
if (!r.files.includes(rel)) continue;
|
|
1145
|
+
}
|
|
1146
|
+
return true;
|
|
1147
|
+
}
|
|
1148
|
+
return false;
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
// src/normalize/checks.ts
|
|
1152
|
+
var RAW_HTML_FUNCTIONS = ["HTML", "FVAL", "TABLE_HTML", "RECS_HTML", "FIELDS_HTML", "PAGE_HTML", "TAGS_HTML", "UNESC_HTML", "ADD_TAGS", "TAG", "VTAG", "ATTR", "BATTR", "STYLE"];
|
|
1153
|
+
var SAFE_VALUE_TYPES = /* @__PURE__ */ new Set(["NUMBER", "CALC", "DATE", "DATETIME", "TIME", "CREATED_TIME", "UPDATED_TIME", "RECORD_NUMBER", "__REVISION__", "__ID__"]);
|
|
1154
|
+
var FORMAT_FUNCTIONS = /* @__PURE__ */ new Set(["FVAL", "DATE_FORMAT", "DATE_ADD", "YEN", "FIXED", "ROUND", "ROUNDUP", "ROUNDDOWN", "INT", "FLOOR", "CEIL", "ABS", "FSIZE", "DURATION_FORMAT", "TODAY", "NOW"]);
|
|
1155
|
+
var STRING_LITERAL = /^"(?:[^"\\]|\\.)*"$/s;
|
|
1156
|
+
var NUMBER_LITERAL = /^-?\d+(?:\.\d+)?$/;
|
|
1157
|
+
var IDENT = /^[\p{L}_$][\p{L}\p{N}_$]*$/u;
|
|
1158
|
+
var isPlainLiteral = (s) => STRING_LITERAL.test(s) && !/[<>&"']/.test(s.slice(1, -1));
|
|
1159
|
+
var isSafeReplacement = (s) => isPlainLiteral(s) || /^"<br\s*\/?>"$/i.test(s);
|
|
1160
|
+
var isSafeSearch = (s) => STRING_LITERAL.test(s) || /^NEWLINE\(\s*\)$/.test(s);
|
|
1161
|
+
function backslashEscapes(formula) {
|
|
1162
|
+
return [...new Set([...formula.matchAll(/"((?:[^"\\]|\\.)*)"/g)].map((m) => m[0]).filter((lit) => /\\[nrt]/.test(lit)))];
|
|
1163
|
+
}
|
|
1164
|
+
function splitTopLevel(s, sep = ",") {
|
|
1165
|
+
const out = [];
|
|
1166
|
+
let depth = 0;
|
|
1167
|
+
let cur = "";
|
|
1168
|
+
let quote = null;
|
|
1169
|
+
for (let i = 0; i < s.length; i++) {
|
|
1170
|
+
const c = s[i];
|
|
1171
|
+
if (quote) {
|
|
1172
|
+
cur += c;
|
|
1173
|
+
if (c === "\\" && i + 1 < s.length) {
|
|
1174
|
+
cur += s[++i];
|
|
1175
|
+
continue;
|
|
1176
|
+
}
|
|
1177
|
+
if (c === quote) quote = null;
|
|
1178
|
+
continue;
|
|
1179
|
+
}
|
|
1180
|
+
if (c === '"' || c === "'") {
|
|
1181
|
+
quote = c;
|
|
1182
|
+
cur += c;
|
|
1183
|
+
continue;
|
|
1184
|
+
}
|
|
1185
|
+
if (c === "(") depth++;
|
|
1186
|
+
else if (c === ")") {
|
|
1187
|
+
depth--;
|
|
1188
|
+
if (depth < 0) return null;
|
|
1189
|
+
}
|
|
1190
|
+
if (c === sep && depth === 0) {
|
|
1191
|
+
out.push(cur.trim());
|
|
1192
|
+
cur = "";
|
|
1193
|
+
continue;
|
|
1194
|
+
}
|
|
1195
|
+
cur += c;
|
|
1196
|
+
}
|
|
1197
|
+
if (depth !== 0 || quote) return null;
|
|
1198
|
+
if (cur.trim() !== "" || out.length) out.push(cur.trim());
|
|
1199
|
+
return out;
|
|
1200
|
+
}
|
|
1201
|
+
function parseCall(e) {
|
|
1202
|
+
const m = e.match(/^([A-Z_][A-Z0-9_]*)\s*\(([\s\S]*)\)$/);
|
|
1203
|
+
if (!m) return null;
|
|
1204
|
+
const args = splitTopLevel(m[2]);
|
|
1205
|
+
if (!args) return null;
|
|
1206
|
+
return { name: m[1], args: args.length === 1 && args[0] === "" ? [] : args };
|
|
1207
|
+
}
|
|
1208
|
+
function commentInsideString(formulaSet) {
|
|
1209
|
+
let inString = false;
|
|
1210
|
+
for (let i = 0; i < formulaSet.length; i++) {
|
|
1211
|
+
const c = formulaSet[i];
|
|
1212
|
+
if (inString) {
|
|
1213
|
+
if (c === "\\" && formulaSet[i + 1] === '"') i++;
|
|
1214
|
+
else if (c === '"') inString = false;
|
|
1215
|
+
else if (c === "/" && formulaSet[i + 1] === "/") return true;
|
|
1216
|
+
continue;
|
|
1217
|
+
}
|
|
1218
|
+
if (c === '"') inString = true;
|
|
1219
|
+
else if (c === "/" && formulaSet[i + 1] === "/") {
|
|
1220
|
+
const nl = formulaSet.indexOf("\n", i);
|
|
1221
|
+
if (nl < 0) return false;
|
|
1222
|
+
i = nl;
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
return false;
|
|
1226
|
+
}
|
|
1227
|
+
function stringLiterals(formula) {
|
|
1228
|
+
return [...formula.matchAll(/"((?:[^"\\]|\\.)*)"|'((?:[^'\\]|\\.)*)'/g)].map((m) => (m[1] ?? m[2] ?? "").replace(/\\(.)/g, "$1"));
|
|
1229
|
+
}
|
|
1230
|
+
function callsOf(formula, name) {
|
|
1231
|
+
const out = [];
|
|
1232
|
+
const re = new RegExp(`\\b${name}\\s*\\(`, "g");
|
|
1233
|
+
let m;
|
|
1234
|
+
while (m = re.exec(formula)) {
|
|
1235
|
+
let depth = 1;
|
|
1236
|
+
let i = m.index + m[0].length;
|
|
1237
|
+
let quote = null;
|
|
1238
|
+
const start = i;
|
|
1239
|
+
for (; i < formula.length; i++) {
|
|
1240
|
+
const c = formula[i];
|
|
1241
|
+
if (quote) {
|
|
1242
|
+
if (c === "\\") i++;
|
|
1243
|
+
else if (c === quote) quote = null;
|
|
1244
|
+
continue;
|
|
1245
|
+
}
|
|
1246
|
+
if (c === '"' || c === "'") quote = c;
|
|
1247
|
+
else if (c === "(") depth++;
|
|
1248
|
+
else if (c === ")") {
|
|
1249
|
+
depth--;
|
|
1250
|
+
if (depth === 0) break;
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1253
|
+
const args = splitTopLevel(formula.slice(start, i));
|
|
1254
|
+
if (args) out.push(args.length === 1 && args[0] === "" ? [] : args);
|
|
1255
|
+
}
|
|
1256
|
+
return out;
|
|
1257
|
+
}
|
|
1258
|
+
function isNumericSafe(arg, pp) {
|
|
1259
|
+
const a = arg.trim();
|
|
1260
|
+
if (!a) return false;
|
|
1261
|
+
const call = parseCall(a);
|
|
1262
|
+
if (call && FORMAT_FUNCTIONS.has(call.name)) return isSafeExpression(a, pp);
|
|
1263
|
+
const tokens = a.match(/[\p{L}_$][\p{L}\p{N}_$]*|\d+(?:\.\d+)?|[+\-*/()]|\s+|[\s\S]/gu) ?? [];
|
|
1264
|
+
for (const t of tokens) {
|
|
1265
|
+
if (/^\s+$/.test(t) || /^[+\-*/()]$/.test(t) || NUMBER_LITERAL.test(t)) continue;
|
|
1266
|
+
if (IDENT.test(t)) {
|
|
1267
|
+
if (!SAFE_VALUE_TYPES.has(pp[t]?.type ?? "")) return false;
|
|
1268
|
+
continue;
|
|
1269
|
+
}
|
|
1270
|
+
return false;
|
|
1271
|
+
}
|
|
1272
|
+
return true;
|
|
1273
|
+
}
|
|
1274
|
+
function isSafeExpression(expr, pp) {
|
|
1275
|
+
const e = expr.trim();
|
|
1276
|
+
if (!e) return false;
|
|
1277
|
+
const terms = splitTopLevel(e, "&");
|
|
1278
|
+
if (terms && terms.length > 1) return terms.every((t) => t !== "" && isSafeExpression(t, pp));
|
|
1279
|
+
if (NUMBER_LITERAL.test(e)) return true;
|
|
1280
|
+
if (STRING_LITERAL.test(e)) return isPlainLiteral(e);
|
|
1281
|
+
if (IDENT.test(e)) return SAFE_VALUE_TYPES.has(pp[e]?.type ?? "");
|
|
1282
|
+
const call = parseCall(e);
|
|
1283
|
+
if (!call) return false;
|
|
1284
|
+
if (call.name === "ESC_HTML") return call.args.length >= 1;
|
|
1285
|
+
if (call.name === "REPLACE") return call.args.length === 3 && parseCall(call.args[0])?.name === "ESC_HTML" && isSafeSearch(call.args[1]) && isSafeReplacement(call.args[2]);
|
|
1286
|
+
if (FORMAT_FUNCTIONS.has(call.name)) {
|
|
1287
|
+
if (call.name === "TODAY" || call.name === "NOW") return call.args.length === 0;
|
|
1288
|
+
if (call.args.length === 0 || !isNumericSafe(call.args[0], pp)) return false;
|
|
1289
|
+
return call.args.slice(1).every((a) => STRING_LITERAL.test(a) ? !/[<>&]/.test(a) : NUMBER_LITERAL.test(a));
|
|
1290
|
+
}
|
|
1291
|
+
return false;
|
|
1292
|
+
}
|
|
1293
|
+
function approvedFinding(f, opt, where, url, via) {
|
|
1294
|
+
if (isAllowed(opt.policy, url, opt.settingsFile)) {
|
|
1295
|
+
f.info("external.allowed", where, `外部の URL(承認済み): ${url}(${via})`);
|
|
1296
|
+
} else {
|
|
1297
|
+
f.warning("external.url", where, `外部の URL: ${url}(${via})。利用者が承認するなら policy/authoring-policy.json の allowExternal に origin か url を書く(AI は書かない)`);
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
function externalFinding(f, ctx, where, url, via, certain = true) {
|
|
1301
|
+
if (ctx.mode === "block") {
|
|
1302
|
+
const msg = `外部の URL: ${url}(${via})。外部参照が「除く」(externalRefs: "block")の設定では、印刷屋は描画の前に kintone 以外への読み込みを除くので帳票に出ない。添付ファイルの #{&f(…)} か data:image/ にする。外部をそのまま使うなら、利用者が externalRefs を "allow" にして policy/authoring-policy.json の allowExternalRefs にこの設定ファイルを書く(AI は書かない)`;
|
|
1303
|
+
if (certain) f.error("external.blocked", where, msg);
|
|
1304
|
+
else f.warning("external.blocked", where, `${msg}。計算式の文字列なので警告(帳票に入れば除かれる)`);
|
|
1305
|
+
return;
|
|
1306
|
+
}
|
|
1307
|
+
approvedFinding(f, ctx, where, url, via);
|
|
1308
|
+
}
|
|
1309
|
+
function escapeFinding(f, where, formulas) {
|
|
1310
|
+
const lits = [...new Set(formulas.flatMap(backslashEscapes))];
|
|
1311
|
+
if (lits.length) f.warning("formula.escape", where, `計算式の文字列は \\ を解釈しない: ${lits.slice(0, 3).join(" ")}("\\n" は改行でなく \\ と n の 2 文字なので、REPLACE(…, "\\n", "<br>") は何も置き換えない)。改行は NEWLINE()(例 REPLACE(ESC_HTML(備考), NEWLINE(), "<br>"))`);
|
|
1312
|
+
}
|
|
1313
|
+
var HTML_LIKE = /<\s*[a-zA-Z!/]/;
|
|
1314
|
+
var CSS_LIKE = /url\s*\(|@import|image-set\s*\(|expression\s*\(|behavior\s*:/i;
|
|
1315
|
+
async function checkFormulaHtml(formula, where, f, ctx) {
|
|
1316
|
+
const raw = RAW_HTML_FUNCTIONS.filter((fn) => new RegExp(`\\b${fn}\\s*\\(`).test(formula));
|
|
1317
|
+
if (raw.length) f.warning("formula.rawHtml", where, `HTML を作る・入れる関数: ${raw.join(", ")}(結果がそのまま帳票の HTML になる。印刷屋が描画の前に外部への読み込みとスクリプトを除くが、崩れは防げない。差分で人が見る)`);
|
|
1318
|
+
const dynamic = [];
|
|
1319
|
+
for (const name of ["TAG", "VTAG"]) {
|
|
1320
|
+
for (const args of callsOf(formula, name)) if (args[0] !== void 0 && !STRING_LITERAL.test(args[0])) dynamic.push(`${name} の要素名 ${args[0]}`);
|
|
1321
|
+
}
|
|
1322
|
+
for (const name of ["ATTR", "STYLE"]) {
|
|
1323
|
+
for (const args of callsOf(formula, name)) {
|
|
1324
|
+
args.forEach((a, i) => {
|
|
1325
|
+
if (a !== "" && !STRING_LITERAL.test(a)) dynamic.push(`${name} の${i % 2 === 0 ? "名前" : "値"} ${a}`);
|
|
1326
|
+
});
|
|
1327
|
+
}
|
|
1328
|
+
}
|
|
1329
|
+
for (const args of callsOf(formula, "BATTR")) for (const a of args) if (a !== "" && !STRING_LITERAL.test(a)) dynamic.push(`BATTR の属性名 ${a}`);
|
|
1330
|
+
if (dynamic.length) {
|
|
1331
|
+
const shown2 = [...new Set(dynamic)];
|
|
1332
|
+
f.warning("formula.attr", where, `要素名や属性にレコードの値や式を入れている: ${shown2.slice(0, 5).join(" / ")}${shown2.length > 5 ? ` 他 ${shown2.length - 5}` : ""}(定数だけにする。印刷屋は描画の前に外部 URL と on 属性を除くが、属性の崩れは防げない)`);
|
|
1333
|
+
}
|
|
1334
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1335
|
+
for (const lit of stringLiterals(formula)) {
|
|
1336
|
+
if (seen.has(lit)) continue;
|
|
1337
|
+
seen.add(lit);
|
|
1338
|
+
if (HTML_LIKE.test(lit)) {
|
|
1339
|
+
const h = await checkHtml(lit, { baseUrl: ctx.baseUrl });
|
|
1340
|
+
for (const e of h.errors) f.warning("formula.html", where, `計算式の文字列の HTML: ${e}(印刷屋が描画の前に外部への読み込みとスクリプトを除く。差分で人が見る)`);
|
|
1341
|
+
for (const u of h.externals) externalFinding(f, ctx, where, u.url, `計算式の文字列 ${u.via}`, false);
|
|
1342
|
+
} else if (CSS_LIKE.test(lit)) {
|
|
1343
|
+
const c = checkCss(/[{}]/.test(lit) ? lit : `x{${lit}}`);
|
|
1344
|
+
for (const e of c.errors) f.warning("formula.html", where, `計算式の文字列の CSS: ${e}(印刷屋が描画の前に外部への読み込みを除く)`);
|
|
1345
|
+
for (const u of c.externals) externalFinding(f, ctx, where, u.url, `計算式の文字列 CSS ${u.via}`, false);
|
|
1346
|
+
}
|
|
1347
|
+
}
|
|
1348
|
+
}
|
|
1349
|
+
async function checkBody(body, model, f, opt) {
|
|
1350
|
+
const { PAPER_NAMES, PAGE_SIZES, DPI_OPTIONS, PRINT_MODES } = model.api;
|
|
1351
|
+
const rows = body.pluginInfos ?? [];
|
|
1352
|
+
const pp = model.pp;
|
|
1353
|
+
const ext = body.externalRefs;
|
|
1354
|
+
const mode = ext === "allow" ? "allow" : "block";
|
|
1355
|
+
const ctx = { ...opt, mode, baseUrl: model.baseUrl || void 0 };
|
|
1356
|
+
if (ext !== "block" && ext !== "allow") {
|
|
1357
|
+
f.error("externalRefs.value", "共通の設定", `externalRefs は "block"(印刷屋が描画の前に kintone 以外への読み込みとスクリプトを除く)か "allow"(何も除かない。自己責任): ${JSON.stringify(ext)}`);
|
|
1358
|
+
} else if (ext === "allow") {
|
|
1359
|
+
if (isExternalRefsAllowed(opt.policy, opt.settingsFile)) {
|
|
1360
|
+
f.info("externalRefs.allowed", "共通の設定", `外部参照は「許可」(利用者が policy/authoring-policy.json の allowExternalRefs で承認済み)。印刷屋は帳票の HTML / CSS から何も除かない(Ver.5 と同じ。自己責任)。外部の URL は allowExternal で承認する`);
|
|
1361
|
+
} else {
|
|
1362
|
+
f.error("externalRefs.unapproved", "共通の設定", `外部参照が「許可」(externalRefs: "allow"。キーが無い Ver.5 の設定も印刷屋は許可で動く)。印刷屋は帳票の HTML / CSS から kintone 以外への読み込みとスクリプトを除かない(自己責任)ので利用者の承認が要る: 利用者が policy/authoring-policy.json の allowExternalRefs にこの設定ファイル(${opt.settingsFile ?? "ファイル名が分からない"})を書く(AI は書かない)。新しい設定は "block"(描画の前に除く)を明示する`);
|
|
1363
|
+
}
|
|
1364
|
+
}
|
|
1365
|
+
for (const [i, c] of (body.cssInfo ?? []).entries()) {
|
|
1366
|
+
if (!c.state || !c.css) continue;
|
|
1367
|
+
const where = `共通 CSS ${i + 1} 行目 (${c.name})`;
|
|
1368
|
+
const r = checkCss(c.css);
|
|
1369
|
+
for (const e of r.errors) f.error("css.rule", where, e);
|
|
1370
|
+
for (const u of r.externals) externalFinding(f, ctx, where, u.url, `CSS ${u.via}`);
|
|
1371
|
+
}
|
|
1372
|
+
const font = body.fontInfo;
|
|
1373
|
+
if (font?.enabled && font.cssUrl) {
|
|
1374
|
+
if (!/^https:\/\//.test(font.cssUrl)) f.error("font.url", "Web フォント", `cssUrl は https の URL: ${font.cssUrl.slice(0, 80)}`);
|
|
1375
|
+
else approvedFinding(f, opt, "Web フォント", font.cssUrl, "fontInfo.cssUrl");
|
|
1376
|
+
}
|
|
1377
|
+
const seenMenus = /* @__PURE__ */ new Map();
|
|
1378
|
+
for (const [i, row] of rows.entries()) {
|
|
1379
|
+
const label = rowLabel(row, i);
|
|
1380
|
+
if (row.state) {
|
|
1381
|
+
const prev = seenMenus.get(row.menu);
|
|
1382
|
+
if (prev !== void 0) f.error("menu.duplicate", label, `有効なボタンの名前が ${prev + 1} 番目と同じ: ${row.menu}(diff と preview の出力が区別できない。名前を変える)`);
|
|
1383
|
+
else seenMenus.set(row.menu, i);
|
|
1384
|
+
}
|
|
1385
|
+
const tags = row.tagsInfo;
|
|
1386
|
+
if (!tags) {
|
|
1387
|
+
if (row.state) f.error("tags.rows", label, "有効なボタンに HTML 設定(tagsInfo)が無い(ボタンは出るが帳票を作れない。無効にするか HTML 設定を書く)");
|
|
1388
|
+
else f.info("tags.missing", label, "無効なボタンに HTML 設定が無い");
|
|
1389
|
+
continue;
|
|
1390
|
+
}
|
|
1391
|
+
if (!PAPER_NAMES.includes(tags.pageSize)) f.error("tags.pageSize", label, `pageSize が使えない値: ${tags.pageSize}(${PAPER_NAMES.join(", ")})`);
|
|
1392
|
+
else if (!PAGE_SIZES.includes(tags.pageSize)) f.warning("tags.pageSize", label, `pageSize ${tags.pageSize} は設定画面の候補に無い(動くが画面で選べない)`);
|
|
1393
|
+
if (tags.orientation !== "p" && tags.orientation !== "l") f.error("tags.orientation", label, `orientation は p か l: ${tags.orientation}`);
|
|
1394
|
+
if (!DPI_OPTIONS.includes(String(tags.dpi))) f.error("tags.dpi", label, `dpi は "96" か "150": ${JSON.stringify(tags.dpi)}`);
|
|
1395
|
+
if (tags.printMode !== void 0 && !PRINT_MODES.includes(tags.printMode)) f.error("tags.printMode", label, `printMode は ${PRINT_MODES.join(" / ")}: ${tags.printMode}`);
|
|
1396
|
+
if (row.list && tags.printMode && tags.printMode !== "preview") f.info("tags.printMode", label, "一覧帳票では printMode は効かない(常にプレビュー)");
|
|
1397
|
+
if (tags.filecode) {
|
|
1398
|
+
const p = model.pp[tags.filecode];
|
|
1399
|
+
if (!p) f.error("tags.filecode", label, `filecode の項目が fields に無い: ${tags.filecode}`);
|
|
1400
|
+
else if (p.type !== "FILE") f.error("tags.filecode", label, `filecode は添付ファイル項目: ${tags.filecode} は ${p.type}`);
|
|
1401
|
+
else if (p.ptcode) f.error("tags.filecode", label, `filecode はテーブルの外の添付ファイル項目: ${tags.filecode} はテーブル ${p.ptcode} の中`);
|
|
1402
|
+
} else if (row.list === false && row.state) {
|
|
1403
|
+
f.info("tags.filecode", label, "保存先が無い(PC はダウンロード、iPhone は共有シート、Android の kintone アプリは案内)");
|
|
1404
|
+
}
|
|
1405
|
+
const fieldsInfo = tags.fieldsInfo;
|
|
1406
|
+
if (fieldsInfo.length < 2) f.error("tags.rows", label, `HTML 設定は 1 行目 $out(ボタン表示条件)、2 行目 $fname(ファイル名)、3 行目以降が帳票。行が ${fieldsInfo.length} しか無い`);
|
|
1407
|
+
if (fieldsInfo[0] && fieldsInfo[0].fieldcode !== "$out") f.error("tags.rows", label, `1 行目の fieldcode は $out: ${fieldsInfo[0].fieldcode}`);
|
|
1408
|
+
if (fieldsInfo[1] && fieldsInfo[1].fieldcode !== "$fname") f.error("tags.rows", label, `2 行目の fieldcode は $fname: ${fieldsInfo[1].fieldcode}`);
|
|
1409
|
+
if (!fieldsInfo.slice(2).some((t) => t.state && (t.html || t.formulaSet))) f.warning("tags.rows", label, "有効な帳票の行(3 行目以降の html か計算式)が無い");
|
|
1410
|
+
for (const [j, t] of fieldsInfo.entries()) {
|
|
1411
|
+
const where = `${label} / ${tagRowLabel(t, j)}`;
|
|
1412
|
+
for (const code of Object.keys(t.usedFields ?? {})) {
|
|
1413
|
+
if (code === UOG_MARK) continue;
|
|
1414
|
+
if (!fieldExists(model, code)) f.error("field.unknown", where, `項目が fields に無い: ${code}(ラベルで書いた、別アプリの項目、綴りの違い)`);
|
|
1415
|
+
}
|
|
1416
|
+
if (!t.state) continue;
|
|
1417
|
+
if (t.formulaSet && commentInsideString(t.formulaSet)) f.error("formula.comment", where, '計算式の文字列の中に // がある("https://…" など)。印刷屋は文字列の中でも // 以降をコメントとして捨てるので式が壊れる。URL は HTML の属性か ##目印## に置く');
|
|
1418
|
+
escapeFinding(f, where, [t.formula || "", ...t.html ? expressionsOf(t.html) : []]);
|
|
1419
|
+
const formula = t.formula || "";
|
|
1420
|
+
if (formula && j >= 2) await checkFormulaHtml(formula, where, f, ctx);
|
|
1421
|
+
if (t.html) {
|
|
1422
|
+
const r = await checkHtml(t.html, { baseUrl: ctx.baseUrl });
|
|
1423
|
+
for (const e of r.errors) f.error("html.rule", where, e);
|
|
1424
|
+
for (const w of r.warnings) f.warning("html.rule", where, w);
|
|
1425
|
+
for (const u of r.externals) externalFinding(f, ctx, where, u.url, u.via);
|
|
1426
|
+
const rawExprs = expressionsOf(t.html).filter((e) => !isSafeExpression(e, pp));
|
|
1427
|
+
if (rawExprs.length) f.warning("html.rawExpression", where, `\${式} が文字列をエスケープせずに差し込んでいる: ${rawExprs.slice(0, 5).map((e) => `\${${e}}`).join(" ")}${rawExprs.length > 5 ? ` 他 ${rawExprs.length - 5}` : ""}(文字列の項目は \${ESC_HTML(項目)}、複数行は \${REPLACE(ESC_HTML(項目), NEWLINE(), "<br>")}。数値・日付の項目と FVAL / DATE_FORMAT はそのままでよい)`);
|
|
1428
|
+
}
|
|
1429
|
+
if (t.css) {
|
|
1430
|
+
const r = checkCss(t.css);
|
|
1431
|
+
for (const e of r.errors) f.error("css.rule", where, e);
|
|
1432
|
+
for (const u of r.externals) externalFinding(f, ctx, where, u.url, `CSS ${u.via}`);
|
|
1433
|
+
}
|
|
1434
|
+
}
|
|
1435
|
+
const outRow = fieldsInfo[0];
|
|
1436
|
+
if (outRow?.state && outRow.usedFields && UOG_MARK in outRow.usedFields && !body.pluginUOG) {
|
|
1437
|
+
f.info("uog.condition", label, "ボタン表示条件で UINFO / OINFO / GINFO を使っているが pluginUOG は偽(更新項目だけで決まる仕様。実機では評価できる)");
|
|
1438
|
+
}
|
|
1439
|
+
for (const [j, c] of row.calcInfo.fieldsInfo.entries()) {
|
|
1440
|
+
if (!c.state) continue;
|
|
1441
|
+
if (c.formulaSet && commentInsideString(c.formulaSet)) f.error("formula.comment", `${label} / 更新項目 ${j + 1} 行目 (${c.fieldcode})`, "計算式の文字列の中に // がある。印刷屋は文字列の中でも // 以降をコメントとして捨てるので式が壊れる");
|
|
1442
|
+
escapeFinding(f, `${label} / 更新項目 ${j + 1} 行目 (${c.fieldcode})`, [c.formula || ""]);
|
|
1443
|
+
for (const code of Object.keys(c.usedFields ?? {})) {
|
|
1444
|
+
if (code === UOG_MARK) continue;
|
|
1445
|
+
if (!fieldExists(model, code)) f.error("field.unknown", `${label} / 更新項目 ${j + 1} 行目 (${c.fieldcode})`, `項目が fields に無い: ${code}`);
|
|
1446
|
+
}
|
|
1447
|
+
}
|
|
1448
|
+
}
|
|
1449
|
+
}
|
|
1450
|
+
|
|
1451
|
+
// src/normalize/size.ts
|
|
1452
|
+
async function measureStored(data, api) {
|
|
1453
|
+
const limits = api.DEFAULT_LIMITS;
|
|
1454
|
+
try {
|
|
1455
|
+
const stored = await api.writeConfig(data, "compressed");
|
|
1456
|
+
const entries = Object.entries(stored);
|
|
1457
|
+
const totalBytes = entries.reduce((n, [k, v]) => n + Buffer.byteLength(k, "utf8") + Buffer.byteLength(v, "utf8"), 0);
|
|
1458
|
+
const split = entries.filter(([k]) => /^json\d*$/.test(k)).length;
|
|
1459
|
+
return { ok: true, totalBytes, maxTotalBytes: limits.maxTotalBytes, splitCount: split, maxValueLength: Math.max(0, ...entries.map(([, v]) => v.length)), maxValueLimit: limits.maxValueLength };
|
|
1460
|
+
} catch (e) {
|
|
1461
|
+
const message = e instanceof api.ConfigStoreError ? e.message : String(e?.message ?? e);
|
|
1462
|
+
return { ok: false, totalBytes: 0, maxTotalBytes: limits.maxTotalBytes, splitCount: 0, maxValueLength: 0, maxValueLimit: limits.maxValueLength, error: message };
|
|
1463
|
+
}
|
|
1464
|
+
}
|
|
1465
|
+
function formatSize(s) {
|
|
1466
|
+
if (!s.ok) return `保存値が kintone の制限を超える: ${s.error}`;
|
|
1467
|
+
const pct = Math.round(s.totalBytes / s.maxTotalBytes * 100);
|
|
1468
|
+
return `保存値(compressed)${s.totalBytes.toLocaleString()} bytes / 上限 ${s.maxTotalBytes.toLocaleString()}(${pct}%。残り ${(s.maxTotalBytes - s.totalBytes).toLocaleString()})、分割 ${s.splitCount}`;
|
|
1469
|
+
}
|
|
1470
|
+
|
|
1471
|
+
// src/commands/normalize.ts
|
|
1472
|
+
var PLUGIN_NAME = "印刷屋プラグイン";
|
|
1473
|
+
var MAX_INPUT_BYTES = 16 * 1024 * 1024;
|
|
1474
|
+
var InputError = class extends Error {
|
|
1475
|
+
};
|
|
1476
|
+
var pad2 = (n) => String(n).padStart(2, "0");
|
|
1477
|
+
function formatDate(d) {
|
|
1478
|
+
return `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())} ${pad2(d.getHours())}:${pad2(d.getMinutes())}:${pad2(d.getSeconds())}`;
|
|
1479
|
+
}
|
|
1480
|
+
function jsonDiff(a, b, pathStr, out) {
|
|
1481
|
+
if (JSON.stringify(a) === JSON.stringify(b)) return;
|
|
1482
|
+
if (a && b && typeof a === "object" && typeof b === "object" && !Array.isArray(a) && !Array.isArray(b)) {
|
|
1483
|
+
for (const k of /* @__PURE__ */ new Set([...Object.keys(a), ...Object.keys(b)])) jsonDiff(a[k], b[k], `${pathStr}.${k}`, out);
|
|
1484
|
+
return;
|
|
1485
|
+
}
|
|
1486
|
+
if (Array.isArray(a) && Array.isArray(b) && a.length === b.length) {
|
|
1487
|
+
a.forEach((x, i) => jsonDiff(x, b[i], `${pathStr}[${i}]`, out));
|
|
1488
|
+
return;
|
|
1489
|
+
}
|
|
1490
|
+
const short = (v) => {
|
|
1491
|
+
const s = JSON.stringify(v);
|
|
1492
|
+
return s === void 0 ? "undefined" : s.length > 100 ? s.slice(0, 100) + "…" : s;
|
|
1493
|
+
};
|
|
1494
|
+
out.push(`${pathStr}: ${short(a)} → ${short(b)}`);
|
|
1495
|
+
}
|
|
1496
|
+
async function normalizeSettings(input) {
|
|
1497
|
+
const f = new Findings();
|
|
1498
|
+
const api = input.engine.api;
|
|
1499
|
+
const pluginVersion = api.pluginVersion;
|
|
1500
|
+
const where = input.settingsFile ?? "settings";
|
|
1501
|
+
let envelope;
|
|
1502
|
+
try {
|
|
1503
|
+
envelope = api.parseJsonSafely(input.settingsText, api.CONFIG_LIMITS);
|
|
1504
|
+
} catch (e) {
|
|
1505
|
+
f.error("json", where, `JSON として読めない、または大きすぎる(${api.CONFIG_LIMITS.maxBytes.toLocaleString()} バイトまで): ${e.message}`);
|
|
1506
|
+
return { findings: f, summary: "JSON を読めない" };
|
|
1507
|
+
}
|
|
1508
|
+
if (!envelope || typeof envelope !== "object" || Array.isArray(envelope)) {
|
|
1509
|
+
f.error("json", where, "JSON の最上位はオブジェクト(封筒形式)");
|
|
1510
|
+
return { findings: f, summary: "封筒形式でない" };
|
|
1511
|
+
}
|
|
1512
|
+
if (envelope.pluginID !== api.pluginId) f.error("envelope.pluginID", where, `pluginID は "${api.pluginId}": ${JSON.stringify(envelope.pluginID)}(封筒なしの素の設定は作らない)`);
|
|
1513
|
+
if (String(envelope.PluginVersion ?? "") !== pluginVersion) f.error("envelope.version", where, `PluginVersion は印刷屋の版 ${pluginVersion}(zip の manifest): ${JSON.stringify(envelope.PluginVersion)}`);
|
|
1514
|
+
if (envelope.appId !== void 0) {
|
|
1515
|
+
if (typeof envelope.appId !== "number" || !Number.isInteger(envelope.appId) || envelope.appId <= 0) f.error("envelope.appId", where, `appId は正の整数(数値): ${JSON.stringify(envelope.appId)}`);
|
|
1516
|
+
else if (envelope.appId !== input.fields.appId) f.warning("envelope.appId", where, `appId ${envelope.appId} が fields のアプリ ${input.fields.appId} と違う`);
|
|
1517
|
+
}
|
|
1518
|
+
if (f.hasErrors) return { findings: f, summary: "封筒の誤り" };
|
|
1519
|
+
const inputBody = bodyOf(envelope);
|
|
1520
|
+
let inputClean;
|
|
1521
|
+
try {
|
|
1522
|
+
inputClean = api.validate(inputBody, api.CONFIG_SCHEMA, api.CONFIG_LIMITS);
|
|
1523
|
+
} catch (e) {
|
|
1524
|
+
if (e instanceof api.ValidationError) {
|
|
1525
|
+
f.error("schema", where, `設定のスキーマに合わない: ${e.message}`);
|
|
1526
|
+
return { findings: f, summary: "スキーマの誤り" };
|
|
1527
|
+
}
|
|
1528
|
+
throw e;
|
|
1529
|
+
}
|
|
1530
|
+
const model = buildModel(input.fields, api, input.baseUrl);
|
|
1531
|
+
if (input.baseUrl && typeof input.fields.baseUrl === "string" && input.fields.baseUrl && model.fieldsBaseUrl !== input.baseUrl) {
|
|
1532
|
+
f.warning("fields.baseUrl", where, `fields の baseUrl(${input.fields.baseUrl.slice(0, 60)})が .env の KINTONE_BASE_URL と違う。iframe の判定は .env の接続先で行う。fields を取り直すなら npx pcraft-authoring fields --app ${input.fields.appId}`);
|
|
1533
|
+
}
|
|
1534
|
+
const body = deriveBody(JSON.parse(JSON.stringify(inputClean)), model, input.engine, f);
|
|
1535
|
+
const policy = input.policy ?? { allowExternal: [] };
|
|
1536
|
+
await checkBody(body, model, f, { policy, settingsFile: input.settingsFile });
|
|
1537
|
+
let cleaned;
|
|
1538
|
+
try {
|
|
1539
|
+
cleaned = api.validate(body, api.CONFIG_SCHEMA, api.CONFIG_LIMITS);
|
|
1540
|
+
} catch (e) {
|
|
1541
|
+
if (e instanceof api.ValidationError) {
|
|
1542
|
+
f.error("schema", where, `正規化した設定がスキーマに合わない: ${e.message}`);
|
|
1543
|
+
return { findings: f, body, summary: "スキーマの誤り" };
|
|
1544
|
+
}
|
|
1545
|
+
throw e;
|
|
1546
|
+
}
|
|
1547
|
+
const size = await measureStored(cleaned, api);
|
|
1548
|
+
if (!size.ok) f.error("size", where, formatSize(size));
|
|
1549
|
+
else f.info("size", where, formatSize(size));
|
|
1550
|
+
let checkDiffs;
|
|
1551
|
+
if (input.check) {
|
|
1552
|
+
checkDiffs = [];
|
|
1553
|
+
jsonDiff(inputClean, cleaned, "$", checkDiffs);
|
|
1554
|
+
}
|
|
1555
|
+
const rows = cleaned.pluginInfos ?? [];
|
|
1556
|
+
const summary = `アプリ ${input.fields.appId} ${input.fields.appName}、ボタン ${rows.length}(有効 ${rows.filter((r) => r.state).length})`;
|
|
1557
|
+
if (f.hasErrors) return { findings: f, body: cleaned, size, checkDiffs, summary };
|
|
1558
|
+
const now = (input.now ?? (() => /* @__PURE__ */ new Date()))();
|
|
1559
|
+
const output = {
|
|
1560
|
+
date: formatDate(now),
|
|
1561
|
+
pluginName: typeof envelope.pluginName === "string" && envelope.pluginName ? envelope.pluginName : PLUGIN_NAME,
|
|
1562
|
+
pluginID: api.pluginId,
|
|
1563
|
+
PluginVersion: pluginVersion,
|
|
1564
|
+
appId: input.fields.appId,
|
|
1565
|
+
appName: typeof envelope.appName === "string" && envelope.appName ? envelope.appName : input.fields.appName
|
|
1566
|
+
};
|
|
1567
|
+
for (const k of ENVELOPE_KEYS) if (!(k in output)) output[k] = envelope[k];
|
|
1568
|
+
Object.assign(output, cleaned);
|
|
1569
|
+
return { findings: f, output, body: cleaned, size, checkDiffs, summary };
|
|
1570
|
+
}
|
|
1571
|
+
function readTextLimited(file, maxBytes = MAX_INPUT_BYTES) {
|
|
1572
|
+
const size = statSync3(file).size;
|
|
1573
|
+
if (size > maxBytes) throw new InputError(`${file} が大きすぎる(${size.toLocaleString()} バイト。上限 ${maxBytes.toLocaleString()})`);
|
|
1574
|
+
return readFileSync4(file, "utf8");
|
|
1575
|
+
}
|
|
1576
|
+
function readJsonLimited(file, maxBytes = MAX_INPUT_BYTES) {
|
|
1577
|
+
let v;
|
|
1578
|
+
try {
|
|
1579
|
+
v = JSON.parse(readTextLimited(file, maxBytes));
|
|
1580
|
+
} catch (e) {
|
|
1581
|
+
if (e instanceof InputError) throw e;
|
|
1582
|
+
throw new InputError(`${file} を JSON として読めない: ${e.message}`);
|
|
1583
|
+
}
|
|
1584
|
+
if (!v || typeof v !== "object" || Array.isArray(v)) throw new InputError(`${file} の最上位はオブジェクト`);
|
|
1585
|
+
return v;
|
|
1586
|
+
}
|
|
1587
|
+
async function readFieldsFile(file) {
|
|
1588
|
+
const fields2 = readJsonLimited(file);
|
|
1589
|
+
if (!fields2.properties || typeof fields2.properties !== "object") throw new InputError(`${file} は fields コマンドの出力ではない(properties が無い)`);
|
|
1590
|
+
if (typeof fields2.appId !== "number" || !Number.isInteger(fields2.appId) || fields2.appId <= 0) throw new InputError(`${file} の appId が正の整数でない`);
|
|
1591
|
+
return fields2;
|
|
1592
|
+
}
|
|
1593
|
+
function relativeSettingsPath(file) {
|
|
1594
|
+
const rel = path5.relative(process.cwd(), path5.resolve(file));
|
|
1595
|
+
return rel.startsWith("..") ? file : rel.replace(/\\/g, "/");
|
|
1596
|
+
}
|
|
1597
|
+
|
|
1598
|
+
// src/commands/take.ts
|
|
1599
|
+
var PRINT_CRAFT_PLUGIN_ID = "rex0220 Print craft plugin";
|
|
1600
|
+
function envFor(ws, appId, envName) {
|
|
1601
|
+
if (envName) return ws.environments[envName] ?? `環境「${envName}」は environments.json に無い`;
|
|
1602
|
+
const hits = Object.values(ws.environments).filter((e) => ws.apps.some((a) => a.ids[e.name] === appId));
|
|
1603
|
+
if (hits.length === 1) return hits[0];
|
|
1604
|
+
if (hits.length > 1) return `アプリ ${appId} が ${hits.map((e) => e.name).join(" と ")} の両方にある。--env で選ぶ`;
|
|
1605
|
+
const all = Object.values(ws.environments);
|
|
1606
|
+
if (all.length === 1) return all[0];
|
|
1607
|
+
return `アプリ ${appId} は environments.json の apps に無く、環境が 2 つ以上ある。apps に足すか --env で選ぶ`;
|
|
1608
|
+
}
|
|
1609
|
+
function takeInbox(cwd, ws, envName) {
|
|
1610
|
+
const result = { moved: [], skipped: [] };
|
|
1611
|
+
const inbox = path6.join(cwd, INBOX);
|
|
1612
|
+
if (!existsSync4(inbox)) return result;
|
|
1613
|
+
for (const name of readdirSync2(inbox).filter((n) => n.toLowerCase().endsWith(".json")).sort()) {
|
|
1614
|
+
const rel = `${INBOX}/${name}`;
|
|
1615
|
+
const src = resolveRead(rel, cwd);
|
|
1616
|
+
let data;
|
|
1617
|
+
try {
|
|
1618
|
+
data = readJsonLimited(src);
|
|
1619
|
+
} catch (e) {
|
|
1620
|
+
result.skipped.push({ file: rel, reason: e.message });
|
|
1621
|
+
continue;
|
|
1622
|
+
}
|
|
1623
|
+
if (data.pluginID !== PRINT_CRAFT_PLUGIN_ID) {
|
|
1624
|
+
result.skipped.push({ file: rel, reason: "印刷屋の設定のファイルではない(pluginID が違う)" });
|
|
1625
|
+
continue;
|
|
1626
|
+
}
|
|
1627
|
+
const appId = data.appId;
|
|
1628
|
+
if (typeof appId !== "number" || !Number.isInteger(appId) || appId <= 0) {
|
|
1629
|
+
result.skipped.push({ file: rel, reason: "封筒の appId が無い" });
|
|
1630
|
+
continue;
|
|
1631
|
+
}
|
|
1632
|
+
const m = name.match(/-app(\d+)-/);
|
|
1633
|
+
if (m && Number(m[1]) !== appId) {
|
|
1634
|
+
result.skipped.push({ file: rel, reason: `ファイル名のアプリ ${m[1]} と封筒の appId ${appId} が違う` });
|
|
1635
|
+
continue;
|
|
1636
|
+
}
|
|
1637
|
+
const env = envFor(ws, appId, envName);
|
|
1638
|
+
if (typeof env === "string") {
|
|
1639
|
+
result.skipped.push({ file: rel, reason: env });
|
|
1640
|
+
continue;
|
|
1641
|
+
}
|
|
1642
|
+
const dir = appDirFor(cwd, env, appId, typeof data.appName === "string" ? data.appName : "");
|
|
1643
|
+
const dest = resolveWrite(path6.join(dir, name), WRITE_ROOTS.kintone, cwd);
|
|
1644
|
+
const text = readFileSync5(src, "utf8");
|
|
1645
|
+
if (existsSync4(dest)) {
|
|
1646
|
+
if (readFileSync5(dest, "utf8") !== text) {
|
|
1647
|
+
result.skipped.push({ file: rel, reason: `行き先に同じ名前の別の中身がある: ${path6.relative(cwd, dest)}` });
|
|
1648
|
+
continue;
|
|
1649
|
+
}
|
|
1650
|
+
unlinkSync(src);
|
|
1651
|
+
result.moved.push({ file: rel, to: path6.relative(cwd, dest), same: true });
|
|
1652
|
+
continue;
|
|
1653
|
+
}
|
|
1654
|
+
mkdirSync(path6.dirname(dest), { recursive: true });
|
|
1655
|
+
writeFileSync(dest, text, "utf8");
|
|
1656
|
+
unlinkSync(src);
|
|
1657
|
+
result.moved.push({ file: rel, to: path6.relative(cwd, dest), same: false });
|
|
1658
|
+
}
|
|
1659
|
+
return result;
|
|
1660
|
+
}
|
|
1661
|
+
|
|
1662
|
+
// src/kintone-rest.ts
|
|
1663
|
+
var ALLOWED_APIS = ["app", "app/form/fields", "app/form/layout", "record", "preview/app/form/fields", "preview/app/form/layout", "app/plugin/config", "preview/app/plugin/config"];
|
|
1664
|
+
var RestError = class extends Error {
|
|
1665
|
+
status;
|
|
1666
|
+
code;
|
|
1667
|
+
apiPath;
|
|
1668
|
+
// Node の型の除去で動かすため、コンストラクターのパラメータープロパティは使わない
|
|
1669
|
+
constructor(message, status, code, apiPath) {
|
|
1670
|
+
super(message);
|
|
1671
|
+
this.status = status;
|
|
1672
|
+
this.code = code;
|
|
1673
|
+
this.apiPath = apiPath;
|
|
1674
|
+
}
|
|
1675
|
+
};
|
|
1676
|
+
var NotAllowedError = class extends Error {
|
|
1677
|
+
};
|
|
1678
|
+
function apiPathOf(api, guestSpaceId) {
|
|
1679
|
+
if (!ALLOWED_APIS.includes(api)) throw new NotAllowedError(`この API は authoring tools では呼ばない: ${api}(許可: ${ALLOWED_APIS.join(", ")})`);
|
|
1680
|
+
if (guestSpaceId !== void 0 && (!Number.isInteger(guestSpaceId) || guestSpaceId <= 0)) throw new NotAllowedError(`ゲストスペースの ID が不正: ${String(guestSpaceId)}`);
|
|
1681
|
+
const prefix = guestSpaceId ? `/k/guest/${guestSpaceId}/v1/` : "/k/v1/";
|
|
1682
|
+
return `${prefix}${api}.json`;
|
|
1683
|
+
}
|
|
1684
|
+
function authHeaders(auth) {
|
|
1685
|
+
if (auth.token) return { "X-Cybozu-API-Token": auth.token };
|
|
1686
|
+
if (auth.username && auth.password) return { "X-Cybozu-Authorization": Buffer.from(`${auth.username}:${auth.password}`, "utf8").toString("base64") };
|
|
1687
|
+
throw new NotAllowedError("認証情報が無い");
|
|
1688
|
+
}
|
|
1689
|
+
function createRestClient(auth, fetchImpl = fetch) {
|
|
1690
|
+
let baseUrl;
|
|
1691
|
+
try {
|
|
1692
|
+
baseUrl = normalizeKintoneBaseUrl(auth.baseUrl);
|
|
1693
|
+
} catch (e) {
|
|
1694
|
+
throw new NotAllowedError(`接続先が kintone ではないので送らない: ${e instanceof KintoneUrlError ? e.message : String(e)}`);
|
|
1695
|
+
}
|
|
1696
|
+
const headers = { ...authHeaders(auth), Accept: "application/json" };
|
|
1697
|
+
return {
|
|
1698
|
+
baseUrl,
|
|
1699
|
+
async get(api, params, guestSpaceId) {
|
|
1700
|
+
const apiPath = apiPathOf(api, guestSpaceId);
|
|
1701
|
+
const url = new URL(apiPath, baseUrl);
|
|
1702
|
+
for (const [k, v] of Object.entries(params)) if (v !== void 0) url.searchParams.set(k, String(v));
|
|
1703
|
+
if (url.origin !== baseUrl || url.username || url.password) throw new NotAllowedError(`送信先が kintone の接続先と違うので送らない: ${url.origin}`);
|
|
1704
|
+
const res = await fetchImpl(url.href, { method: "GET", headers });
|
|
1705
|
+
const text = await res.text();
|
|
1706
|
+
if (!res.ok) {
|
|
1707
|
+
let code;
|
|
1708
|
+
try {
|
|
1709
|
+
const body = JSON.parse(text);
|
|
1710
|
+
code = typeof body.code === "string" ? body.code.slice(0, 40) : void 0;
|
|
1711
|
+
} catch {
|
|
1712
|
+
}
|
|
1713
|
+
const hint = res.status === 401 ? "(認証情報かアクセス権を確かめる。API トークンならそのアプリで有効か)" : res.status === 403 ? "(アクセス権が無い)" : res.status === 404 ? "(アプリかレコードが無い)" : "";
|
|
1714
|
+
throw new RestError(`kintone ${apiPath} が HTTP ${res.status}${code ? ` ${code}` : ""}${hint}`, res.status, code, apiPath);
|
|
1715
|
+
}
|
|
1716
|
+
return JSON.parse(text);
|
|
1717
|
+
}
|
|
1718
|
+
};
|
|
1719
|
+
}
|
|
1720
|
+
|
|
1721
|
+
// src/commands/fields.ts
|
|
1722
|
+
async function fetchFields(client2, opt) {
|
|
1723
|
+
const lang = opt.lang ?? "ja";
|
|
1724
|
+
const app = await client2.get("app", { id: opt.app }, opt.guestSpaceId);
|
|
1725
|
+
const fields2 = await client2.get(opt.preview ? "preview/app/form/fields" : "app/form/fields", { app: opt.app, lang }, opt.guestSpaceId);
|
|
1726
|
+
const layout = await client2.get(opt.preview ? "preview/app/form/layout" : "app/form/layout", { app: opt.app }, opt.guestSpaceId);
|
|
1727
|
+
return {
|
|
1728
|
+
tool: "pcraft-authoring fields",
|
|
1729
|
+
fetchedAt: (opt.now ?? (() => /* @__PURE__ */ new Date()))().toISOString(),
|
|
1730
|
+
baseUrl: client2.baseUrl,
|
|
1731
|
+
appId: Number(app.appId),
|
|
1732
|
+
appName: app.name,
|
|
1733
|
+
...opt.guestSpaceId ? { guestSpaceId: opt.guestSpaceId } : {},
|
|
1734
|
+
preview: !!opt.preview,
|
|
1735
|
+
lang,
|
|
1736
|
+
revision: String(fields2.revision ?? layout.revision ?? ""),
|
|
1737
|
+
properties: fields2.properties ?? {},
|
|
1738
|
+
layout: layout.layout ?? []
|
|
1739
|
+
};
|
|
1740
|
+
}
|
|
1741
|
+
function layoutOrder(layout) {
|
|
1742
|
+
const out = [];
|
|
1743
|
+
const walk = (rows) => {
|
|
1744
|
+
for (const r of rows ?? []) {
|
|
1745
|
+
if (r.type === "GROUP") {
|
|
1746
|
+
if (r.code) out.push(r.code);
|
|
1747
|
+
walk(r.layout);
|
|
1748
|
+
} else if (r.type === "SUBTABLE") {
|
|
1749
|
+
if (r.code) out.push(r.code);
|
|
1750
|
+
} else {
|
|
1751
|
+
for (const f of r.fields ?? []) if (f.code) out.push(f.code);
|
|
1752
|
+
}
|
|
1753
|
+
}
|
|
1754
|
+
};
|
|
1755
|
+
walk(layout);
|
|
1756
|
+
return out;
|
|
1757
|
+
}
|
|
1758
|
+
var SELECT_TYPES = /* @__PURE__ */ new Set(["CHECK_BOX", "RADIO_BUTTON", "DROP_DOWN", "MULTI_SELECT"]);
|
|
1759
|
+
function fieldNotes(p, inTable, copyTargets) {
|
|
1760
|
+
const notes = [];
|
|
1761
|
+
if (p.type === "CALC" && typeof p.format === "string") notes.push(`書式 ${p.format}`);
|
|
1762
|
+
if (p.type === "SINGLE_LINE_TEXT" && typeof p.expression === "string" && p.expression) notes.push("自動計算");
|
|
1763
|
+
if (p.type === "NUMBER" || p.type === "CALC") {
|
|
1764
|
+
if (p.digit === true) notes.push("桁区切り");
|
|
1765
|
+
if (p.displayScale !== void 0 && p.displayScale !== "") notes.push(`小数 ${p.displayScale} 桁`);
|
|
1766
|
+
if (typeof p.unit === "string" && p.unit) notes.push(`単位「${p.unit}」${p.unitPosition === "BEFORE" ? "前" : "後"}`);
|
|
1767
|
+
}
|
|
1768
|
+
if (SELECT_TYPES.has(p.type) && p.options && typeof p.options === "object") {
|
|
1769
|
+
const labels = Object.values(p.options).sort((a, b) => Number(a.index) - Number(b.index)).map((o) => o.label);
|
|
1770
|
+
notes.push(`選択肢 ${labels.slice(0, 8).join(" / ")}${labels.length > 8 ? ` 他 ${labels.length - 8}` : ""}`);
|
|
1771
|
+
}
|
|
1772
|
+
const lookup = p.lookup;
|
|
1773
|
+
if (lookup && typeof lookup === "object") notes.push(`ルックアップ(アプリ ${lookup.relatedApp?.app ?? "?"} の ${lookup.relatedKeyField ?? "?"})`);
|
|
1774
|
+
if (copyTargets.has(p.code)) notes.push("ルックアップのコピー先");
|
|
1775
|
+
if (p.type === "FILE" && !inTable) notes.push("保存先 filecode にできる");
|
|
1776
|
+
return notes.length ? `(${notes.join("、")})` : "";
|
|
1777
|
+
}
|
|
1778
|
+
function listFields(file) {
|
|
1779
|
+
const props = file.properties;
|
|
1780
|
+
const copyTargets = /* @__PURE__ */ new Set();
|
|
1781
|
+
const collect = (p) => {
|
|
1782
|
+
const maps = p.lookup?.fieldMappings;
|
|
1783
|
+
for (const m of maps ?? []) if (m.field) copyTargets.add(m.field);
|
|
1784
|
+
for (const c of Object.values(p.fields ?? {})) collect(c);
|
|
1785
|
+
};
|
|
1786
|
+
for (const p of Object.values(props)) collect(p);
|
|
1787
|
+
const line = (p, indent, inTable) => `${indent}${p.code}${p.label && p.label !== p.code ? `「${p.label}」` : ""} ${p.type}${fieldNotes(p, inTable, copyTargets)}`;
|
|
1788
|
+
const lines = [];
|
|
1789
|
+
const shown2 = /* @__PURE__ */ new Set();
|
|
1790
|
+
for (const code of layoutOrder(file.layout)) {
|
|
1791
|
+
const p = props[code];
|
|
1792
|
+
if (!p || shown2.has(code)) continue;
|
|
1793
|
+
shown2.add(code);
|
|
1794
|
+
lines.push(line(p, "", false));
|
|
1795
|
+
if (p.type === "SUBTABLE") for (const c of Object.values(p.fields ?? {})) lines.push(line(c, " ", true));
|
|
1796
|
+
}
|
|
1797
|
+
const rest = Object.values(props).filter((p) => !shown2.has(p.code));
|
|
1798
|
+
const header = `アプリ ${file.appId} ${file.appName}(${file.preview ? "preview" : "運用中"}、lang=${file.lang}、取得 ${file.fetchedAt})項目 ${Object.keys(props).length}。レイアウトの順、テーブルの子は字下げ(TABLE_HTML と計算式で使う。更新項目にはできない)`;
|
|
1799
|
+
const tail = rest.length ? [`レイアウトに無い項目: ${rest.map((p) => `${p.code} ${p.type}`).join(", ")}`] : [];
|
|
1800
|
+
return [header, ...lines, ...tail].join("\n");
|
|
1801
|
+
}
|
|
1802
|
+
function summarizeFields(file) {
|
|
1803
|
+
const props = Object.values(file.properties);
|
|
1804
|
+
const tables = props.filter((p) => p.type === "SUBTABLE");
|
|
1805
|
+
const byType = /* @__PURE__ */ new Map();
|
|
1806
|
+
for (const p of props) byType.set(p.type, (byType.get(p.type) ?? 0) + 1);
|
|
1807
|
+
const types = [...byType.entries()].sort((a, b) => b[1] - a[1]).map(([t, n]) => `${t} ${n}`).join(", ");
|
|
1808
|
+
const tableLines = tables.map((t) => ` テーブル ${t.code}: ${Object.keys(t.fields ?? {}).join(", ")}`);
|
|
1809
|
+
return [`アプリ ${file.appId} ${file.appName}(${file.preview ? "preview" : "運用中"}、lang=${file.lang})`, `項目 ${props.length}(${types})`, ...tableLines].join("\n");
|
|
1810
|
+
}
|
|
1811
|
+
|
|
1812
|
+
// src/commands/record.ts
|
|
1813
|
+
var ALWAYS = /* @__PURE__ */ new Set(["$id", "$revision"]);
|
|
1814
|
+
function identifiersOf(expr) {
|
|
1815
|
+
return expr.replace(/"[^"]*"/g, " ").split(/[^\p{L}\p{N}_・_$¥$]+/u).filter((t) => t && !/^\d+$/.test(t));
|
|
1816
|
+
}
|
|
1817
|
+
function usedFieldCodes(settings) {
|
|
1818
|
+
const codes = /* @__PURE__ */ new Set();
|
|
1819
|
+
const s = settings;
|
|
1820
|
+
if (!s || typeof s !== "object") return null;
|
|
1821
|
+
const addKeys = (o) => {
|
|
1822
|
+
if (o && typeof o === "object") for (const k of Object.keys(o)) codes.add(k);
|
|
1823
|
+
};
|
|
1824
|
+
const addWords = (text) => {
|
|
1825
|
+
if (typeof text === "string") for (const w of identifiersOf(text)) codes.add(w);
|
|
1826
|
+
};
|
|
1827
|
+
const addFormula = (f) => addWords(typeof f.formula === "string" && f.formula ? f.formula : f.formulaSet);
|
|
1828
|
+
addKeys(s.usedFields);
|
|
1829
|
+
for (const row of s.pluginInfos ?? []) {
|
|
1830
|
+
const tags = row.tagsInfo;
|
|
1831
|
+
for (const f of tags?.fieldsInfo ?? []) {
|
|
1832
|
+
addKeys(f.usedFields);
|
|
1833
|
+
addFormula(f);
|
|
1834
|
+
if (typeof f.html === "string") for (const e of expressionsOf(f.html)) addWords(e);
|
|
1835
|
+
}
|
|
1836
|
+
if (tags?.filecode) codes.add(tags.filecode);
|
|
1837
|
+
const calc = row.calcInfo;
|
|
1838
|
+
addKeys(calc?.usedFields);
|
|
1839
|
+
for (const f of calc?.fieldsInfo ?? []) {
|
|
1840
|
+
if (f.state && typeof f.fieldcode === "string" && !f.fieldcode.startsWith("$")) codes.add(f.fieldcode);
|
|
1841
|
+
addKeys(f.usedFields);
|
|
1842
|
+
addFormula(f);
|
|
1843
|
+
}
|
|
1844
|
+
}
|
|
1845
|
+
for (const c of [...codes]) if (c.startsWith("$")) codes.delete(c);
|
|
1846
|
+
return codes.size ? codes : null;
|
|
1847
|
+
}
|
|
1848
|
+
function narrowRecord(record2, keep) {
|
|
1849
|
+
const out = {};
|
|
1850
|
+
const kept = [];
|
|
1851
|
+
for (const [code, fv] of Object.entries(record2)) {
|
|
1852
|
+
if (ALWAYS.has(code)) {
|
|
1853
|
+
out[code] = fv;
|
|
1854
|
+
continue;
|
|
1855
|
+
}
|
|
1856
|
+
if (fv?.type === "SUBTABLE" && Array.isArray(fv.value)) {
|
|
1857
|
+
const rows = fv.value;
|
|
1858
|
+
const childCodes = new Set(rows.flatMap((r) => Object.keys(r.value ?? {})));
|
|
1859
|
+
const wholeTable = keep.has(code);
|
|
1860
|
+
const usedChildren = [...childCodes].filter((c) => keep.has(c));
|
|
1861
|
+
if (!wholeTable && usedChildren.length === 0) continue;
|
|
1862
|
+
out[code] = {
|
|
1863
|
+
type: "SUBTABLE",
|
|
1864
|
+
value: rows.map((r) => {
|
|
1865
|
+
if (wholeTable) return r;
|
|
1866
|
+
const v = {};
|
|
1867
|
+
for (const c of usedChildren) if (r.value[c]) v[c] = r.value[c];
|
|
1868
|
+
return { ...r.id ? { id: r.id } : {}, value: v };
|
|
1869
|
+
})
|
|
1870
|
+
};
|
|
1871
|
+
kept.push(wholeTable ? code : `${code}(${usedChildren.join(",")})`);
|
|
1872
|
+
continue;
|
|
1873
|
+
}
|
|
1874
|
+
if (keep.has(code)) {
|
|
1875
|
+
out[code] = fv;
|
|
1876
|
+
kept.push(code);
|
|
1877
|
+
}
|
|
1878
|
+
}
|
|
1879
|
+
return { record: out, kept };
|
|
1880
|
+
}
|
|
1881
|
+
function missingInRecord(settings, fields2, record2) {
|
|
1882
|
+
const used = usedFieldCodes(settings);
|
|
1883
|
+
if (!used) return [];
|
|
1884
|
+
const missing = [];
|
|
1885
|
+
for (const [code, p] of Object.entries(fields2.properties ?? {})) {
|
|
1886
|
+
if (p.type === "SUBTABLE") {
|
|
1887
|
+
const table = record2[code];
|
|
1888
|
+
const rows = Array.isArray(table?.value) ? table.value : null;
|
|
1889
|
+
for (const child of Object.keys(p.fields ?? {})) {
|
|
1890
|
+
if (!used.has(child)) continue;
|
|
1891
|
+
if (!rows || rows.length > 0 && !rows.some((r) => r.value && child in r.value)) missing.push(`${code}.${child}`);
|
|
1892
|
+
}
|
|
1893
|
+
if (used.has(code) && !table) missing.push(code);
|
|
1894
|
+
continue;
|
|
1895
|
+
}
|
|
1896
|
+
if (used.has(code) && !(code in record2)) missing.push(code);
|
|
1897
|
+
}
|
|
1898
|
+
return [...new Set(missing)];
|
|
1899
|
+
}
|
|
1900
|
+
async function fetchRecord(client2, opt) {
|
|
1901
|
+
const res = await client2.get("record", { app: opt.app, id: opt.id }, opt.guestSpaceId);
|
|
1902
|
+
let record2 = res.record ?? {};
|
|
1903
|
+
let keptFields;
|
|
1904
|
+
if (opt.keep) {
|
|
1905
|
+
const n = narrowRecord(record2, opt.keep);
|
|
1906
|
+
record2 = n.record;
|
|
1907
|
+
keptFields = n.kept;
|
|
1908
|
+
}
|
|
1909
|
+
return {
|
|
1910
|
+
tool: "pcraft-authoring record",
|
|
1911
|
+
fetchedAt: (opt.now ?? (() => /* @__PURE__ */ new Date()))().toISOString(),
|
|
1912
|
+
baseUrl: client2.baseUrl,
|
|
1913
|
+
appId: opt.app,
|
|
1914
|
+
id: opt.id,
|
|
1915
|
+
...opt.guestSpaceId ? { guestSpaceId: opt.guestSpaceId } : {},
|
|
1916
|
+
...keptFields ? { keptFields } : {},
|
|
1917
|
+
record: record2
|
|
1918
|
+
};
|
|
1919
|
+
}
|
|
1920
|
+
var MULTI_TYPES = /* @__PURE__ */ new Set(["CHECK_BOX", "MULTI_SELECT", "CATEGORY"]);
|
|
1921
|
+
var ENTITY_TYPES = /* @__PURE__ */ new Set(["USER_SELECT", "ORGANIZATION_SELECT", "GROUP_SELECT", "STATUS_ASSIGNEE"]);
|
|
1922
|
+
var DATE_TYPES = { DATE: "日付", DATETIME: "日時", CREATED_TIME: "日時", UPDATED_TIME: "日時", TIME: "時刻" };
|
|
1923
|
+
var NUMERIC = /^-?\d+(\.\d+)?$/;
|
|
1924
|
+
function shapeOf(fv) {
|
|
1925
|
+
const v = fv.value;
|
|
1926
|
+
if (v === null || v === void 0 || v === "" || Array.isArray(v) && v.length === 0) return "空";
|
|
1927
|
+
if (fv.type === "FILE" && Array.isArray(v)) {
|
|
1928
|
+
const byType = /* @__PURE__ */ new Map();
|
|
1929
|
+
for (const f of v) byType.set(f.contentType ?? "?", (byType.get(f.contentType ?? "?") ?? 0) + 1);
|
|
1930
|
+
return `${v.length} 件(${[...byType].map(([t, n]) => `${t} ${n}`).join("、")})`;
|
|
1931
|
+
}
|
|
1932
|
+
if (MULTI_TYPES.has(fv.type) && Array.isArray(v)) return `${v.length} 個`;
|
|
1933
|
+
if (ENTITY_TYPES.has(fv.type) && Array.isArray(v)) return `${v.length} 件`;
|
|
1934
|
+
if (DATE_TYPES[fv.type]) return DATE_TYPES[fv.type];
|
|
1935
|
+
if (fv.type === "RECORD_NUMBER") return "あり";
|
|
1936
|
+
if (typeof v === "string") {
|
|
1937
|
+
if ((fv.type === "NUMBER" || fv.type === "CALC") && NUMERIC.test(v)) {
|
|
1938
|
+
const [int, dec] = v.replace("-", "").split(".");
|
|
1939
|
+
return `数値 ${v.startsWith("-") ? "負 " : ""}整数 ${int.length} 桁${dec ? `、小数 ${dec.length} 桁` : ""}`;
|
|
1940
|
+
}
|
|
1941
|
+
const lines = v.split("\n").length;
|
|
1942
|
+
return `${fv.type === "RICH_TEXT" ? "HTML " : ""}${[...v].length} 文字${lines > 1 ? `、${lines} 行` : ""}`;
|
|
1943
|
+
}
|
|
1944
|
+
if (typeof v === "object") return "あり";
|
|
1945
|
+
return typeof v;
|
|
1946
|
+
}
|
|
1947
|
+
function columnShape(type, values) {
|
|
1948
|
+
const shapes = values.map((value) => shapeOf({ type, value }));
|
|
1949
|
+
const filled = shapes.filter((s) => s !== "空");
|
|
1950
|
+
if (!filled.length) return "全行 空";
|
|
1951
|
+
const longest = filled.reduce((a, b) => Number(b.match(/\d+/)?.[0] ?? 0) > Number(a.match(/\d+/)?.[0] ?? 0) ? b : a);
|
|
1952
|
+
const multiLine = filled.filter((s) => /\d+ 行$/.test(s)).length;
|
|
1953
|
+
return `最大 ${longest}${multiLine ? `(複数行の値 ${multiLine} 行)` : ""}${filled.length < shapes.length ? `、空 ${shapes.length - filled.length} 行` : ""}`;
|
|
1954
|
+
}
|
|
1955
|
+
function describeRecord(file) {
|
|
1956
|
+
const lines = [];
|
|
1957
|
+
for (const [code, fv] of Object.entries(file.record)) {
|
|
1958
|
+
if (code === "$id" || code === "$revision" || !fv) continue;
|
|
1959
|
+
if (fv.type === "SUBTABLE" && Array.isArray(fv.value)) {
|
|
1960
|
+
const rows = fv.value;
|
|
1961
|
+
lines.push(`${code} SUBTABLE ${rows.length} 行`);
|
|
1962
|
+
const children = /* @__PURE__ */ new Map();
|
|
1963
|
+
for (const r of rows) {
|
|
1964
|
+
for (const [c, cv] of Object.entries(r.value ?? {})) {
|
|
1965
|
+
const e = children.get(c) ?? { type: cv.type, values: [] };
|
|
1966
|
+
e.values.push(cv.value);
|
|
1967
|
+
children.set(c, e);
|
|
1968
|
+
}
|
|
1969
|
+
}
|
|
1970
|
+
for (const [c, e] of children) lines.push(` ${c} ${e.type} ${columnShape(e.type, e.values)}`);
|
|
1971
|
+
continue;
|
|
1972
|
+
}
|
|
1973
|
+
lines.push(`${code} ${fv.type} ${shapeOf(fv)}`);
|
|
1974
|
+
}
|
|
1975
|
+
const header = `アプリ ${file.appId} レコード ${file.id}(取得 ${file.fetchedAt}${file.keptFields ? `、--fields-from で ${file.keptFields.length} 項目に絞った` : ""})。値は出さない(形だけ)`;
|
|
1976
|
+
return [header, ...lines].join("\n");
|
|
1977
|
+
}
|
|
1978
|
+
function summarizeRecord(file) {
|
|
1979
|
+
const codes = Object.keys(file.record);
|
|
1980
|
+
const tables = codes.filter((c) => file.record[c]?.type === "SUBTABLE");
|
|
1981
|
+
const rows = tables.map((t) => `${t} ${Array.isArray(file.record[t].value) ? file.record[t].value.length : 0} 行`);
|
|
1982
|
+
return `アプリ ${file.appId} レコード ${file.id}: 項目 ${codes.length}${tables.length ? `(テーブル ${rows.join(", ")})` : ""}${file.keptFields ? `。--fields-from で ${file.keptFields.length} 項目に絞った` : ""}。値は表示しない`;
|
|
1983
|
+
}
|
|
1984
|
+
|
|
1985
|
+
// src/normalize/line-diff.ts
|
|
1986
|
+
var MAX_CELLS = 4e6;
|
|
1987
|
+
function lineDiff(a, b) {
|
|
1988
|
+
const A = a.split("\n");
|
|
1989
|
+
const B = b.split("\n");
|
|
1990
|
+
if (A.length * B.length > MAX_CELLS) {
|
|
1991
|
+
return [...A.map((t) => ({ kind: "-", text: t })), ...B.map((t) => ({ kind: "+", text: t }))];
|
|
1992
|
+
}
|
|
1993
|
+
const n = A.length;
|
|
1994
|
+
const m = B.length;
|
|
1995
|
+
const dp = [];
|
|
1996
|
+
for (let i2 = 0; i2 <= n; i2++) dp.push(new Uint32Array(m + 1));
|
|
1997
|
+
for (let i2 = n - 1; i2 >= 0; i2--) for (let j2 = m - 1; j2 >= 0; j2--) dp[i2][j2] = A[i2] === B[j2] ? dp[i2 + 1][j2 + 1] + 1 : Math.max(dp[i2 + 1][j2], dp[i2][j2 + 1]);
|
|
1998
|
+
const out = [];
|
|
1999
|
+
let i = 0;
|
|
2000
|
+
let j = 0;
|
|
2001
|
+
while (i < n && j < m) {
|
|
2002
|
+
if (A[i] === B[j]) {
|
|
2003
|
+
out.push({ kind: " ", text: A[i] });
|
|
2004
|
+
i++;
|
|
2005
|
+
j++;
|
|
2006
|
+
} else if (dp[i + 1][j] >= dp[i][j + 1]) {
|
|
2007
|
+
out.push({ kind: "-", text: A[i] });
|
|
2008
|
+
i++;
|
|
2009
|
+
} else {
|
|
2010
|
+
out.push({ kind: "+", text: B[j] });
|
|
2011
|
+
j++;
|
|
2012
|
+
}
|
|
2013
|
+
}
|
|
2014
|
+
while (i < n) out.push({ kind: "-", text: A[i++] });
|
|
2015
|
+
while (j < m) out.push({ kind: "+", text: B[j++] });
|
|
2016
|
+
return out;
|
|
2017
|
+
}
|
|
2018
|
+
function formatDiff(lines, context2 = 2) {
|
|
2019
|
+
const keep = new Array(lines.length).fill(false);
|
|
2020
|
+
lines.forEach((l, i) => {
|
|
2021
|
+
if (l.kind === " ") return;
|
|
2022
|
+
for (let k = Math.max(0, i - context2); k <= Math.min(lines.length - 1, i + context2); k++) keep[k] = true;
|
|
2023
|
+
});
|
|
2024
|
+
const out = [];
|
|
2025
|
+
let skipping = false;
|
|
2026
|
+
lines.forEach((l, i) => {
|
|
2027
|
+
if (!keep[i]) {
|
|
2028
|
+
if (!skipping) out.push(" …");
|
|
2029
|
+
skipping = true;
|
|
2030
|
+
return;
|
|
2031
|
+
}
|
|
2032
|
+
skipping = false;
|
|
2033
|
+
out.push(`${l.kind} ${l.text}`);
|
|
2034
|
+
});
|
|
2035
|
+
return out.join("\n");
|
|
2036
|
+
}
|
|
2037
|
+
|
|
2038
|
+
// src/commands/buttons.ts
|
|
2039
|
+
import { createHash } from "node:crypto";
|
|
2040
|
+
var ButtonNotFoundError = class extends Error {
|
|
2041
|
+
};
|
|
2042
|
+
var kb = (v) => `${(Buffer.byteLength(JSON.stringify(v) ?? "", "utf8") / 1024).toFixed(1)} KB`;
|
|
2043
|
+
var oneLine = (s, max = 160) => {
|
|
2044
|
+
const t = s.replace(/\s+/g, " ").trim();
|
|
2045
|
+
return t.length > max ? `${t.slice(0, max)}…` : t;
|
|
2046
|
+
};
|
|
2047
|
+
var chars = (s) => [...s ?? ""].length;
|
|
2048
|
+
function shortenDataUrls(s, opt = {}) {
|
|
2049
|
+
return s.replace(/data:[^"'\s)<>]{120,}/g, (m) => `${m.slice(0, 40)}…(data URL ${m.length.toLocaleString()} 文字${opt.hash ? `、sha256 ${createHash("sha256").update(m).digest("hex").slice(0, 8)}` : ""})`);
|
|
2050
|
+
}
|
|
2051
|
+
function screensOf(b) {
|
|
2052
|
+
const ids = (b.viewsCsv ?? "").split(",").map((s) => s.trim()).filter(Boolean);
|
|
2053
|
+
const lists = ids.filter((x) => x !== "-");
|
|
2054
|
+
const listText = ids.length === 0 ? "すべての一覧" : lists.length ? `一覧 ${lists.join(",")}` : "";
|
|
2055
|
+
if (b.list) return `一覧帳票(${listText || "どの一覧にも出ない"})`;
|
|
2056
|
+
const detail2 = ids.length === 0 || ids.includes("-");
|
|
2057
|
+
return [detail2 ? "詳細画面" : "", listText ? `${listText}で一括処理` : ""].filter(Boolean).join(" + ") || "どの画面にも出ない";
|
|
2058
|
+
}
|
|
2059
|
+
function headline(b, i) {
|
|
2060
|
+
const t = b.tagsInfo ?? {};
|
|
2061
|
+
const parts = [
|
|
2062
|
+
b.state ? "有効" : "無効",
|
|
2063
|
+
screensOf(b),
|
|
2064
|
+
t.filecode ? `保存先 ${t.filecode}` : "保存先なし(ダウンロード)",
|
|
2065
|
+
`${t.pageSize ?? "?"} ${t.orientation === "l" ? "横" : t.orientation === "p" ? "縦" : t.orientation ?? "?"} ${t.dpi ?? "?"} dpi`,
|
|
2066
|
+
`押したとき ${t.printMode ?? "preview"}`,
|
|
2067
|
+
...b.authority ? ["権限で絞る"] : [],
|
|
2068
|
+
kb(b)
|
|
2069
|
+
];
|
|
2070
|
+
return `${i + 1}. ${b.menu ?? "(名前なし)"}(${parts.join("、")})`;
|
|
2071
|
+
}
|
|
2072
|
+
function overview(b) {
|
|
2073
|
+
const rows = b.tagsInfo?.fieldsInfo ?? [];
|
|
2074
|
+
const [out, fname, ...body] = rows;
|
|
2075
|
+
const lines = [];
|
|
2076
|
+
lines.push(` 表示条件: ${out ? out.state && out.formulaSet?.trim() ? oneLine(out.formulaSet) : "なし(常に出す)" : "(1 行目が無い)"}`);
|
|
2077
|
+
lines.push(` ファイル名: ${fname ? fname.state && fname.formulaSet?.trim() ? oneLine(fname.formulaSet) : "なし" : "(2 行目が無い)"}`);
|
|
2078
|
+
const bodies = body.map((r, j) => `${j + 3} 行目 ${r.desc || "(説明なし)"}${r.state ? "" : "(無効)"}: HTML ${chars(r.html).toLocaleString()} 文字、CSS ${chars(r.css).toLocaleString()} 文字、計算式 ${(r.formulaSet ?? "").trim() ? (r.formulaSet ?? "").split("\n").length : 0} 行`);
|
|
2079
|
+
lines.push(` 帳票: ${bodies.length ? bodies.join(" / ") : "なし"}`);
|
|
2080
|
+
const calc = (b.calcInfo?.fieldsInfo ?? []).filter((c) => c.fieldcode !== "$out");
|
|
2081
|
+
lines.push(` 更新項目: ${calc.length ? calc.map((c) => `${c.fieldcode}${c.state ? "" : "(無効)"}`).join(", ") : "なし"}`);
|
|
2082
|
+
return lines;
|
|
2083
|
+
}
|
|
2084
|
+
function detail(b, i) {
|
|
2085
|
+
const lines = [headline(b, i)];
|
|
2086
|
+
for (const k of ["menu_en", "menu_ja", "menu_zh", "desc", "desc_en", "desc_ja", "desc_zh", "remark", "users", "organizations", "groups", "guest"]) {
|
|
2087
|
+
const v = b[k];
|
|
2088
|
+
if (typeof v === "string" && v) lines.push(`${k}: ${oneLine(v, 400)}`);
|
|
2089
|
+
}
|
|
2090
|
+
for (const [j, r] of (b.tagsInfo?.fieldsInfo ?? []).entries()) {
|
|
2091
|
+
const role = j === 0 ? "$out ボタン表示条件" : j === 1 ? "$fname ファイル名" : "帳票";
|
|
2092
|
+
lines.push(`--- HTML 設定 ${j + 1} 行目 ${role}(${r.state ? "有効" : "無効"})${r.desc ? ` ${r.desc}` : ""}${r.remark ? ` 備考: ${oneLine(r.remark, 200)}` : ""}`);
|
|
2093
|
+
if (j >= 2 && r.css) lines.push("CSS:", shortenDataUrls(r.css));
|
|
2094
|
+
if (j >= 2 && r.html) lines.push("HTML:", shortenDataUrls(r.html));
|
|
2095
|
+
if (r.formulaSet) lines.push("計算式:", shortenDataUrls(r.formulaSet));
|
|
2096
|
+
}
|
|
2097
|
+
const calc = b.calcInfo?.fieldsInfo ?? [];
|
|
2098
|
+
lines.push(`--- 更新項目 ${calc.length ? `${calc.length} 行` : "なし"}`);
|
|
2099
|
+
for (const [j, c] of calc.entries()) lines.push(`${j + 1} 行目 ${c.fieldcode}(${c.state ? "有効" : "無効"}): ${shortenDataUrls(c.formulaSet ?? "")}${c.remark ? ` 備考: ${oneLine(c.remark, 200)}` : ""}`);
|
|
2100
|
+
return lines;
|
|
2101
|
+
}
|
|
2102
|
+
function listButtons(settings, opt) {
|
|
2103
|
+
const buttons2 = Array.isArray(settings.pluginInfos) ? settings.pluginInfos : [];
|
|
2104
|
+
if (opt.button !== void 0) {
|
|
2105
|
+
const hits = buttons2.map((b, i) => ({ b, i })).filter(({ b }) => b.menu === opt.button);
|
|
2106
|
+
if (!hits.length) throw new ButtonNotFoundError(`ボタン「${opt.button}」は ${opt.file} に無い(${buttons2.map((b) => b.menu).join(", ") || "ボタンなし"})`);
|
|
2107
|
+
return [...hits.flatMap(({ b, i }) => detail(b, i)), "(data: の URL は先頭と長さだけ。ファイルには元のまま)"].join("\n");
|
|
2108
|
+
}
|
|
2109
|
+
const normalized = buttons2.some((b) => (b.tagsInfo?.fieldsInfo ?? []).some((r) => typeof r.formula === "string"));
|
|
2110
|
+
const font = settings.fontInfo;
|
|
2111
|
+
const css = Array.isArray(settings.cssInfo) ? settings.cssInfo.length : 0;
|
|
2112
|
+
const ext = settings.externalRefs;
|
|
2113
|
+
const header = [
|
|
2114
|
+
settings.appId !== void 0 ? `アプリ ${String(settings.appId)} ${typeof settings.appName === "string" ? settings.appName : ""}`.trim() : "アプリ不明",
|
|
2115
|
+
`ボタン ${buttons2.length}(有効 ${buttons2.filter((b) => b.state).length})`,
|
|
2116
|
+
`外部参照 ${ext === "block" || ext === "allow" ? ext : ext === void 0 ? "キー無し(印刷屋は allow で動く)" : JSON.stringify(ext)}`,
|
|
2117
|
+
`共通 CSS ${settings.commonCssEnable === false ? "無効" : css ? `${css} 行` : "既定(省略)"}`,
|
|
2118
|
+
`Web フォント ${font?.enabled ? font.family ?? "有効" : "なし"}`,
|
|
2119
|
+
normalized ? "派生値あり(normalize 済み)" : "派生値なし",
|
|
2120
|
+
kb(settings)
|
|
2121
|
+
];
|
|
2122
|
+
const lines = [`${opt.file}(${header.join("、")})`];
|
|
2123
|
+
for (const [i, b] of buttons2.entries()) lines.push(headline(b, i), ...overview(b));
|
|
2124
|
+
lines.push("中身は --button <名前>");
|
|
2125
|
+
return lines.join("\n");
|
|
2126
|
+
}
|
|
2127
|
+
|
|
2128
|
+
// src/commands/diff.ts
|
|
2129
|
+
var ROW_SCALARS = ["state", "menu", "menu_en", "menu_ja", "menu_zh", "mcheck", "list", "authority", "guest", "users", "organizations", "groups", "desc", "desc_en", "desc_ja", "desc_zh", "remark", "viewsCsv"];
|
|
2130
|
+
var TAGS_SCALARS = ["filecode", "pageSize", "orientation", "dpi", "printMode"];
|
|
2131
|
+
var TAG_ROW_SCALARS = ["state", "desc", "remark", "fieldcode"];
|
|
2132
|
+
var TAG_ROW_TEXTS = ["css", "html", "formulaSet"];
|
|
2133
|
+
var show = (v) => v === void 0 ? "(無し)" : JSON.stringify(v);
|
|
2134
|
+
function scalarDiffs(a, b, keys, out, prefix) {
|
|
2135
|
+
for (const k of keys) {
|
|
2136
|
+
const x = a?.[k];
|
|
2137
|
+
const y = b?.[k];
|
|
2138
|
+
if (JSON.stringify(x) !== JSON.stringify(y)) out.push(`${prefix}${k}: ${show(x)} → ${show(y)}`);
|
|
2139
|
+
}
|
|
2140
|
+
}
|
|
2141
|
+
function textDiff(label, a, b, out) {
|
|
2142
|
+
if (a === b) return;
|
|
2143
|
+
out.push(`${label}:`);
|
|
2144
|
+
out.push(formatDiff(lineDiff(a, b)));
|
|
2145
|
+
}
|
|
2146
|
+
function diffSettings(beforeEnvelope, afterEnvelope, opt = {}) {
|
|
2147
|
+
const out = [];
|
|
2148
|
+
const a = bodyOf(beforeEnvelope);
|
|
2149
|
+
const b = bodyOf(afterEnvelope);
|
|
2150
|
+
for (const k of ["appId", "appName", "PluginVersion"]) if (JSON.stringify(beforeEnvelope[k]) !== JSON.stringify(afterEnvelope[k])) out.push(`封筒 ${k}: ${show(beforeEnvelope[k])} → ${show(afterEnvelope[k])}`);
|
|
2151
|
+
scalarDiffs(a, b, ["pluginEnable", "pluginComment", "pluginDescription", "commonCssEnable", "externalRefs"], out, "");
|
|
2152
|
+
if (JSON.stringify(a.menuInfo) !== JSON.stringify(b.menuInfo)) out.push(`menuInfo: ${show(a.menuInfo)} → ${show(b.menuInfo)}`);
|
|
2153
|
+
if (JSON.stringify(a.fontInfo) !== JSON.stringify(b.fontInfo)) out.push(`Web フォント: ${show(a.fontInfo)} → ${show(b.fontInfo)}`);
|
|
2154
|
+
if (JSON.stringify(a.guestsInfo) !== JSON.stringify(b.guestsInfo)) out.push(`ゲスト: ${show(a.guestsInfo)} → ${show(b.guestsInfo)}`);
|
|
2155
|
+
const cssA = new Map((a.cssInfo ?? []).map((c, i) => [String(c.name ?? i), c]));
|
|
2156
|
+
const cssB = new Map((b.cssInfo ?? []).map((c, i) => [String(c.name ?? i), c]));
|
|
2157
|
+
for (const name of /* @__PURE__ */ new Set([...cssA.keys(), ...cssB.keys()])) {
|
|
2158
|
+
const x = cssA.get(name);
|
|
2159
|
+
const y = cssB.get(name);
|
|
2160
|
+
if (!x) out.push(`共通 CSS ${name}: 追加`);
|
|
2161
|
+
else if (!y) out.push(`共通 CSS ${name}: 削除`);
|
|
2162
|
+
else {
|
|
2163
|
+
scalarDiffs(x, y, ["state", "desc"], out, `共通 CSS ${name} / `);
|
|
2164
|
+
textDiff(`共通 CSS ${name} / css`, String(x.css ?? ""), String(y.css ?? ""), out);
|
|
2165
|
+
}
|
|
2166
|
+
}
|
|
2167
|
+
const key = (r, i, seen) => {
|
|
2168
|
+
const base = String(r.menu ?? `#${i + 1}`);
|
|
2169
|
+
const n = (seen.get(base) ?? 0) + 1;
|
|
2170
|
+
seen.set(base, n);
|
|
2171
|
+
return n === 1 ? base : `${base} (${n})`;
|
|
2172
|
+
};
|
|
2173
|
+
const seenA = /* @__PURE__ */ new Map();
|
|
2174
|
+
const seenB = /* @__PURE__ */ new Map();
|
|
2175
|
+
const rowsA = new Map((a.pluginInfos ?? []).map((r, i) => [key(r, i, seenA), r]));
|
|
2176
|
+
const rowsB = new Map((b.pluginInfos ?? []).map((r, i) => [key(r, i, seenB), r]));
|
|
2177
|
+
for (const name of /* @__PURE__ */ new Set([...rowsA.keys(), ...rowsB.keys()])) {
|
|
2178
|
+
const x = rowsA.get(name);
|
|
2179
|
+
const y = rowsB.get(name);
|
|
2180
|
+
const p = `ボタン ${name} / `;
|
|
2181
|
+
if (!x) {
|
|
2182
|
+
out.push(`ボタン ${name}: 追加`);
|
|
2183
|
+
continue;
|
|
2184
|
+
}
|
|
2185
|
+
if (!y) {
|
|
2186
|
+
out.push(`ボタン ${name}: 削除`);
|
|
2187
|
+
continue;
|
|
2188
|
+
}
|
|
2189
|
+
scalarDiffs(x, y, ROW_SCALARS, out, p);
|
|
2190
|
+
const tx = x.tagsInfo;
|
|
2191
|
+
const ty = y.tagsInfo;
|
|
2192
|
+
if (!!tx !== !!ty) out.push(`${p}HTML 設定: ${tx ? "削除" : "追加"}`);
|
|
2193
|
+
else if (tx && ty) {
|
|
2194
|
+
scalarDiffs(tx, ty, TAGS_SCALARS, out, `${p}HTML 設定 / `);
|
|
2195
|
+
const fa = tx.fieldsInfo ?? [];
|
|
2196
|
+
const fb = ty.fieldsInfo ?? [];
|
|
2197
|
+
for (let i = 0; i < Math.max(fa.length, fb.length); i++) {
|
|
2198
|
+
const ra = fa[i];
|
|
2199
|
+
const rb = fb[i];
|
|
2200
|
+
const q = `${p}HTML 設定 ${i + 1} 行目 / `;
|
|
2201
|
+
if (!ra) {
|
|
2202
|
+
out.push(`${q}追加 (${show(rb?.desc)})`);
|
|
2203
|
+
continue;
|
|
2204
|
+
}
|
|
2205
|
+
if (!rb) {
|
|
2206
|
+
out.push(`${q}削除 (${show(ra.desc)})`);
|
|
2207
|
+
continue;
|
|
2208
|
+
}
|
|
2209
|
+
scalarDiffs(ra, rb, TAG_ROW_SCALARS, out, q);
|
|
2210
|
+
for (const k of TAG_ROW_TEXTS) textDiff(`${q}${k}`, String(ra[k] ?? ""), String(rb[k] ?? ""), out);
|
|
2211
|
+
if (opt.derived) scalarDiffs(ra, rb, ["id", "formula", "usedFields"], out, `${q}派生 `);
|
|
2212
|
+
}
|
|
2213
|
+
}
|
|
2214
|
+
const ca = new Map((x.calcInfo?.fieldsInfo ?? []).map((c) => [String(c.fieldcode), c]));
|
|
2215
|
+
const cb = new Map((y.calcInfo?.fieldsInfo ?? []).map((c) => [String(c.fieldcode), c]));
|
|
2216
|
+
for (const code of /* @__PURE__ */ new Set([...ca.keys(), ...cb.keys()])) {
|
|
2217
|
+
const u = ca.get(code);
|
|
2218
|
+
const v = cb.get(code);
|
|
2219
|
+
const q = `${p}更新項目 ${code} / `;
|
|
2220
|
+
if (!u) {
|
|
2221
|
+
if (v?.state || v?.formulaSet) out.push(`${q}追加(state ${show(v?.state)})`);
|
|
2222
|
+
continue;
|
|
2223
|
+
}
|
|
2224
|
+
if (!v) {
|
|
2225
|
+
if (u.state || u.formulaSet) out.push(`${q}削除`);
|
|
2226
|
+
continue;
|
|
2227
|
+
}
|
|
2228
|
+
scalarDiffs(u, v, ["state", "remark"], out, q);
|
|
2229
|
+
textDiff(`${q}formulaSet`, String(u.formulaSet ?? ""), String(v.formulaSet ?? ""), out);
|
|
2230
|
+
if (opt.derived) scalarDiffs(u, v, ["formula", "usedFields", "type", "ptcode"], out, `${q}派生 `);
|
|
2231
|
+
}
|
|
2232
|
+
if (opt.derived) scalarDiffs(x, y, ["id", "views"], out, `${p}派生 `);
|
|
2233
|
+
}
|
|
2234
|
+
if (opt.derived) scalarDiffs(a, b, ["usedFields", "pluginUOG"], out, "派生 ");
|
|
2235
|
+
const derivedNote = opt.derived ? "" : "(派生値 formula / usedFields / id / views / pluginUOG は除く。--derived で含める)";
|
|
2236
|
+
const body = shortenDataUrls(out.join("\n"), { hash: true });
|
|
2237
|
+
const dataNote = body.includes("…(data URL ") ? "(data: の URL は先頭・長さ・sha256 の先頭だけ)" : "";
|
|
2238
|
+
return out.length ? `${body}
|
|
2239
|
+
${derivedNote}${dataNote}`.trim() : `差分なし${derivedNote}`;
|
|
2240
|
+
}
|
|
2241
|
+
|
|
2242
|
+
// src/commands/pull.ts
|
|
2243
|
+
var PLUGIN_ID_PATTERN = /^[a-p]{32}$/;
|
|
2244
|
+
async function pullSettings(client2, engine, opt) {
|
|
2245
|
+
if (!PLUGIN_ID_PATTERN.test(opt.pluginId)) throw new InputError(`プラグイン ID の形が違う: ${opt.pluginId}(a〜p の 32 文字)`);
|
|
2246
|
+
const app = await client2.get("app", { id: opt.app }, opt.guestSpaceId);
|
|
2247
|
+
const res = await client2.get(opt.preview ? "preview/app/plugin/config" : "app/plugin/config", { app: opt.app, id: opt.pluginId }, opt.guestSpaceId);
|
|
2248
|
+
const stored = res.config && typeof res.config === "object" && !Array.isArray(res.config) ? res.config : {};
|
|
2249
|
+
if (Object.keys(stored).length === 0) throw new InputError(`アプリ ${opt.app} の印刷屋の設定が空(${opt.preview ? "動作テスト環境" : "運用中"}。設定画面でまだ保存していないか、${opt.preview ? "" : "保存して運用環境に反映していないか、"}別のプラグイン ID)`);
|
|
2250
|
+
for (const [k, v] of Object.entries(stored)) if (typeof v !== "string") throw new InputError(`プラグインの設定の値が文字列でない: ${k}`);
|
|
2251
|
+
const api = engine.api;
|
|
2252
|
+
const { data, format } = await api.readConfig(stored);
|
|
2253
|
+
const raw = data && typeof data === "object" && !Array.isArray(data) ? data : {};
|
|
2254
|
+
const body = api.validate(raw, api.CONFIG_SCHEMA, api.CONFIG_LIMITS);
|
|
2255
|
+
const envelope = api.buildExportData({ pluginName: PLUGIN_NAME, pluginId: api.pluginId, pluginVersion: engine.source.pluginVersion, appId: opt.app, appName: app.name }, body, (opt.now ?? (() => /* @__PURE__ */ new Date()))());
|
|
2256
|
+
return { envelope, appName: app.name, revision: String(res.revision ?? ""), format };
|
|
2257
|
+
}
|
|
2258
|
+
function defaultPullName(appName, appId) {
|
|
2259
|
+
return `${safeFileName(appName, `app-${appId}`)}.json`;
|
|
2260
|
+
}
|
|
2261
|
+
|
|
2262
|
+
// src/engine.ts
|
|
2263
|
+
import { existsSync as existsSync6, readFileSync as readFileSync8 } from "node:fs";
|
|
2264
|
+
import path9 from "node:path";
|
|
2265
|
+
import vm from "node:vm";
|
|
2266
|
+
|
|
2267
|
+
// src/paths.ts
|
|
2268
|
+
import { existsSync as existsSync5, realpathSync as realpathSync2 } from "node:fs";
|
|
2269
|
+
import path7 from "node:path";
|
|
2270
|
+
import { fileURLToPath } from "node:url";
|
|
2271
|
+
var here = path7.dirname(fileURLToPath(import.meta.url));
|
|
2272
|
+
var AUTHORING_ROOT = path7.resolve(here, "..");
|
|
2273
|
+
var REPO_ROOT = path7.resolve(AUTHORING_ROOT, "..");
|
|
2274
|
+
var VENDOR_DIR = path7.join(AUTHORING_ROOT, "vendor");
|
|
2275
|
+
var MOMENT_FILE = path7.join(VENDOR_DIR, "moment-with-locales.min.js");
|
|
2276
|
+
function linkedRoot(name, envVar, fallback) {
|
|
2277
|
+
const env = process.env[envVar];
|
|
2278
|
+
if (env) return env;
|
|
2279
|
+
const linked = path7.join(AUTHORING_ROOT, "node_modules", name);
|
|
2280
|
+
if (existsSync5(path7.join(linked, "package.json"))) {
|
|
2281
|
+
try {
|
|
2282
|
+
return realpathSync2(linked);
|
|
2283
|
+
} catch {
|
|
2284
|
+
return linked;
|
|
2285
|
+
}
|
|
2286
|
+
}
|
|
2287
|
+
return fallback;
|
|
2288
|
+
}
|
|
2289
|
+
var PRINT_CRAFT_ROOT = linkedRoot("print-craft", "PCRAFT_PRINT_CRAFT_ROOT", path7.resolve(REPO_ROOT, "..", "print-craft"));
|
|
2290
|
+
function devPluginDir() {
|
|
2291
|
+
const dir = path7.join(PRINT_CRAFT_ROOT, "prod");
|
|
2292
|
+
return existsSync5(path7.join(dir, "desktop_js", "KintoneFormulaPCraft.min.js")) ? dir : null;
|
|
2293
|
+
}
|
|
2294
|
+
|
|
2295
|
+
// src/plugin-zip.ts
|
|
2296
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
2297
|
+
import { readFileSync as readFileSync6, statSync as statSync4 } from "node:fs";
|
|
2298
|
+
import { inflateRawSync } from "node:zlib";
|
|
2299
|
+
var SIG_EOCD = 101010256;
|
|
2300
|
+
var SIG_CENTRAL = 33639248;
|
|
2301
|
+
var SIG_LOCAL = 67324752;
|
|
2302
|
+
var PluginZipError = class extends Error {
|
|
2303
|
+
};
|
|
2304
|
+
var ZIP_LIMITS = {
|
|
2305
|
+
maxOuterBytes: 64 * 1024 * 1024,
|
|
2306
|
+
maxEntries: 500,
|
|
2307
|
+
maxEntryBytes: 32 * 1024 * 1024,
|
|
2308
|
+
maxTotalBytes: 128 * 1024 * 1024
|
|
2309
|
+
};
|
|
2310
|
+
var CRC_TABLE = (() => {
|
|
2311
|
+
const t = new Uint32Array(256);
|
|
2312
|
+
for (let n = 0; n < 256; n++) {
|
|
2313
|
+
let c = n;
|
|
2314
|
+
for (let k = 0; k < 8; k++) c = c & 1 ? 3988292384 ^ c >>> 1 : c >>> 1;
|
|
2315
|
+
t[n] = c >>> 0;
|
|
2316
|
+
}
|
|
2317
|
+
return t;
|
|
2318
|
+
})();
|
|
2319
|
+
function crc32(buf) {
|
|
2320
|
+
let c = 4294967295;
|
|
2321
|
+
for (let i = 0; i < buf.length; i++) c = CRC_TABLE[(c ^ buf[i]) & 255] ^ c >>> 8;
|
|
2322
|
+
return (c ^ 4294967295) >>> 0;
|
|
2323
|
+
}
|
|
2324
|
+
function u16(buf, p, what) {
|
|
2325
|
+
if (p < 0 || p + 2 > buf.length) throw new PluginZipError(`zip が壊れている(${what} が範囲外)`);
|
|
2326
|
+
return buf.readUInt16LE(p);
|
|
2327
|
+
}
|
|
2328
|
+
function u32(buf, p, what) {
|
|
2329
|
+
if (p < 0 || p + 4 > buf.length) throw new PluginZipError(`zip が壊れている(${what} が範囲外)`);
|
|
2330
|
+
return buf.readUInt32LE(p);
|
|
2331
|
+
}
|
|
2332
|
+
function unzip(buf, limits = ZIP_LIMITS) {
|
|
2333
|
+
if (buf.length > limits.maxOuterBytes) throw new PluginZipError(`zip が大きすぎる(${buf.length} バイト。上限 ${limits.maxOuterBytes})`);
|
|
2334
|
+
const minEocd = 22;
|
|
2335
|
+
let eocd = -1;
|
|
2336
|
+
for (let i = buf.length - minEocd; i >= Math.max(0, buf.length - minEocd - 65535); i--) {
|
|
2337
|
+
if (buf.readUInt32LE(i) === SIG_EOCD) {
|
|
2338
|
+
eocd = i;
|
|
2339
|
+
break;
|
|
2340
|
+
}
|
|
2341
|
+
}
|
|
2342
|
+
if (eocd < 0) throw new PluginZipError("zip の終端(EOCD)が無い");
|
|
2343
|
+
const total = u16(buf, eocd + 10, "entry 数");
|
|
2344
|
+
const cdSize = u32(buf, eocd + 12, "central directory の大きさ");
|
|
2345
|
+
const cdOffset = u32(buf, eocd + 16, "central directory の位置");
|
|
2346
|
+
if (total > limits.maxEntries) throw new PluginZipError(`zip の entry が多すぎる(${total}。上限 ${limits.maxEntries})`);
|
|
2347
|
+
if (cdOffset + cdSize > eocd) throw new PluginZipError("zip が壊れている(central directory が終端を越える)");
|
|
2348
|
+
const out = /* @__PURE__ */ new Map();
|
|
2349
|
+
let p = cdOffset;
|
|
2350
|
+
let totalBytes = 0;
|
|
2351
|
+
for (let n = 0; n < total; n++) {
|
|
2352
|
+
if (u32(buf, p, "central directory") !== SIG_CENTRAL) throw new PluginZipError(`zip の central directory が壊れている(entry ${n})`);
|
|
2353
|
+
const method = u16(buf, p + 10, "圧縮方式");
|
|
2354
|
+
const crc = u32(buf, p + 16, "CRC");
|
|
2355
|
+
const compSize = u32(buf, p + 20, "圧縮後の大きさ");
|
|
2356
|
+
const uncompSize = u32(buf, p + 24, "展開後の大きさ");
|
|
2357
|
+
const nameLen = u16(buf, p + 28, "名前の長さ");
|
|
2358
|
+
const extraLen = u16(buf, p + 30, "extra の長さ");
|
|
2359
|
+
const commentLen = u16(buf, p + 32, "コメントの長さ");
|
|
2360
|
+
const localOffset = u32(buf, p + 42, "local header の位置");
|
|
2361
|
+
if (p + 46 + nameLen > eocd) throw new PluginZipError(`zip が壊れている(entry ${n} の名前が範囲外)`);
|
|
2362
|
+
const name = buf.subarray(p + 46, p + 46 + nameLen).toString("utf8");
|
|
2363
|
+
p += 46 + nameLen + extraLen + commentLen;
|
|
2364
|
+
if (name.endsWith("/")) continue;
|
|
2365
|
+
if (name.startsWith("/") || name.split("/").includes("..")) throw new PluginZipError(`zip の entry 名が不正: ${name}`);
|
|
2366
|
+
if (out.has(name)) throw new PluginZipError(`zip に同じ名前の entry が 2 つある: ${name}`);
|
|
2367
|
+
if (uncompSize > limits.maxEntryBytes) throw new PluginZipError(`zip の entry が大きすぎる: ${name}(${uncompSize} バイト。上限 ${limits.maxEntryBytes})`);
|
|
2368
|
+
totalBytes += uncompSize;
|
|
2369
|
+
if (totalBytes > limits.maxTotalBytes) throw new PluginZipError(`zip の展開後の合計が大きすぎる(上限 ${limits.maxTotalBytes} バイト)`);
|
|
2370
|
+
if (u32(buf, localOffset, `local header(${name})`) !== SIG_LOCAL) throw new PluginZipError(`zip の local header が壊れている: ${name}`);
|
|
2371
|
+
const localNameLen = u16(buf, localOffset + 26, "local の名前の長さ");
|
|
2372
|
+
const localExtraLen = u16(buf, localOffset + 28, "local の extra の長さ");
|
|
2373
|
+
const localName = buf.subarray(localOffset + 30, localOffset + 30 + localNameLen).toString("utf8");
|
|
2374
|
+
if (localName !== name) throw new PluginZipError(`zip の entry 名が central directory と local header で違う: ${name} / ${localName}`);
|
|
2375
|
+
const dataStart = localOffset + 30 + localNameLen + localExtraLen;
|
|
2376
|
+
if (dataStart + compSize > cdOffset) throw new PluginZipError(`zip が壊れている(${name} のデータが central directory に重なる)`);
|
|
2377
|
+
const data = buf.subarray(dataStart, dataStart + compSize);
|
|
2378
|
+
let content;
|
|
2379
|
+
if (method === 0) content = Buffer.from(data);
|
|
2380
|
+
else if (method === 8) {
|
|
2381
|
+
try {
|
|
2382
|
+
content = inflateRawSync(data, { maxOutputLength: Math.max(1, uncompSize) });
|
|
2383
|
+
} catch (e) {
|
|
2384
|
+
throw new PluginZipError(`zip の entry を展開できない: ${name}(${e instanceof Error ? e.message : String(e)})`);
|
|
2385
|
+
}
|
|
2386
|
+
} else throw new PluginZipError(`zip の圧縮方式 ${method} には対応していない: ${name}`);
|
|
2387
|
+
if (content.length !== uncompSize) throw new PluginZipError(`zip の entry の大きさが合わない: ${name}(${content.length} / ${uncompSize})`);
|
|
2388
|
+
if (crc32(content) !== crc) throw new PluginZipError(`zip の entry の CRC が合わない(壊れているか改変されている): ${name}`);
|
|
2389
|
+
out.set(name, content);
|
|
2390
|
+
}
|
|
2391
|
+
return out;
|
|
2392
|
+
}
|
|
2393
|
+
var ENGINE_ENTRY = "desktop_js/KintoneFormulaPCraft.min.js";
|
|
2394
|
+
var BIGNUMBER_ENTRY = "desktop_js/bignumber.min.js";
|
|
2395
|
+
var MOMENT_TZ_ENTRY = "desktop_js/moment-timezone-with-data.min.js";
|
|
2396
|
+
var API_ENTRY = "config_js/print-craft-authoring-api.js";
|
|
2397
|
+
var MANIFEST_ENTRY = "manifest.json";
|
|
2398
|
+
function sha256Hex(data) {
|
|
2399
|
+
return createHash2("sha256").update(data).digest("hex");
|
|
2400
|
+
}
|
|
2401
|
+
function pluginIdOf(publicKey) {
|
|
2402
|
+
return sha256Hex(publicKey).slice(0, 32).replace(/[0-9a-f]/g, (c) => "abcdefghijklmnop"["0123456789abcdef".indexOf(c)]);
|
|
2403
|
+
}
|
|
2404
|
+
function readPluginZip(file) {
|
|
2405
|
+
let outer;
|
|
2406
|
+
try {
|
|
2407
|
+
const size = statSync4(file).size;
|
|
2408
|
+
if (size > ZIP_LIMITS.maxOuterBytes) throw new PluginZipError(`大きすぎる(${size} バイト)`);
|
|
2409
|
+
outer = unzip(readFileSync6(file));
|
|
2410
|
+
} catch (e) {
|
|
2411
|
+
throw new PluginZipError(`印刷屋の zip を読めない: ${file}(${e instanceof Error ? e.message : String(e)})`);
|
|
2412
|
+
}
|
|
2413
|
+
const contents = outer.get("contents.zip");
|
|
2414
|
+
if (!contents) throw new PluginZipError(`印刷屋の zip ではない(contents.zip が無い): ${file}`);
|
|
2415
|
+
let inner;
|
|
2416
|
+
try {
|
|
2417
|
+
inner = unzip(contents);
|
|
2418
|
+
} catch (e) {
|
|
2419
|
+
throw new PluginZipError(`印刷屋の zip の contents.zip を読めない: ${file}(${e instanceof Error ? e.message : String(e)})`);
|
|
2420
|
+
}
|
|
2421
|
+
const need = (name) => {
|
|
2422
|
+
const b = inner.get(name);
|
|
2423
|
+
if (!b) throw new PluginZipError(`印刷屋の zip に ${name} が無い: ${file}`);
|
|
2424
|
+
return b;
|
|
2425
|
+
};
|
|
2426
|
+
let manifest;
|
|
2427
|
+
try {
|
|
2428
|
+
manifest = JSON.parse(need(MANIFEST_ENTRY).toString("utf8"));
|
|
2429
|
+
} catch (e) {
|
|
2430
|
+
throw new PluginZipError(`印刷屋の zip の manifest.json を読めない: ${file}(${e instanceof Error ? e.message : String(e)})`);
|
|
2431
|
+
}
|
|
2432
|
+
if (!manifest || typeof manifest !== "object") throw new PluginZipError(`印刷屋の zip の manifest.json が不正: ${file}`);
|
|
2433
|
+
const engine = need(ENGINE_ENTRY);
|
|
2434
|
+
const bignumber = need(BIGNUMBER_ENTRY);
|
|
2435
|
+
const momentTimezone = need(MOMENT_TZ_ENTRY);
|
|
2436
|
+
const api = inner.get(API_ENTRY);
|
|
2437
|
+
const pubkey = outer.get("PUBKEY");
|
|
2438
|
+
return {
|
|
2439
|
+
from: file,
|
|
2440
|
+
...pubkey ? { pluginId: pluginIdOf(pubkey) } : {},
|
|
2441
|
+
manifest,
|
|
2442
|
+
pluginVersion: String(manifest.version ?? ""),
|
|
2443
|
+
engine: engine.toString("utf8"),
|
|
2444
|
+
bignumber: bignumber.toString("utf8"),
|
|
2445
|
+
momentTimezone: momentTimezone.toString("utf8"),
|
|
2446
|
+
api: api?.toString("utf8"),
|
|
2447
|
+
sha256: {
|
|
2448
|
+
engine: sha256Hex(engine),
|
|
2449
|
+
...api ? { api: sha256Hex(api) } : {},
|
|
2450
|
+
bignumber: sha256Hex(bignumber),
|
|
2451
|
+
momentTimezone: sha256Hex(momentTimezone),
|
|
2452
|
+
contents: sha256Hex(contents)
|
|
2453
|
+
}
|
|
2454
|
+
};
|
|
2455
|
+
}
|
|
2456
|
+
|
|
2457
|
+
// src/meta.ts
|
|
2458
|
+
import { execFileSync } from "node:child_process";
|
|
2459
|
+
import { createHash as createHash3 } from "node:crypto";
|
|
2460
|
+
import { readFileSync as readFileSync7 } from "node:fs";
|
|
2461
|
+
import path8 from "node:path";
|
|
2462
|
+
var SUPPORTED_PLUGIN_VERSIONS = ["6"];
|
|
2463
|
+
var SUPPORTED_API_VERSION = 1;
|
|
2464
|
+
var KNOWN_PLUGIN_RELEASES = {
|
|
2465
|
+
"6": [
|
|
2466
|
+
{
|
|
2467
|
+
engine: "8a5f78f78f51e04b5daaa435b0e8a8c0ea4d08d388d84d279f0cd1b50a857140",
|
|
2468
|
+
api: "59c27e480f419b9d3480c24426ae93dadf0ce7c46c940bcf7457d8252b26dea3",
|
|
2469
|
+
bignumber: "eca7c1c71fee589d7b5c58bd3df3f31d56fbd234821ed8c60ec4e2050ec50129",
|
|
2470
|
+
momentTimezone: "31b9bea01ffef2e8f311eafdbbcdd944a12194fa216d8f54489e15a7188d47dc",
|
|
2471
|
+
note: "Ver.6(2026-10-04 の PR #2 マージ時点。print-craft 73097d2。描画前の掃除が入る前)"
|
|
2472
|
+
},
|
|
2473
|
+
{
|
|
2474
|
+
engine: "8a5f78f78f51e04b5daaa435b0e8a8c0ea4d08d388d84d279f0cd1b50a857140",
|
|
2475
|
+
api: "3c029ba0bb2b2c869de9dc074e588cf3e5cae8caac91a83e3da196ef2aeee673",
|
|
2476
|
+
bignumber: "eca7c1c71fee589d7b5c58bd3df3f31d56fbd234821ed8c60ec4e2050ec50129",
|
|
2477
|
+
momentTimezone: "31b9bea01ffef2e8f311eafdbbcdd944a12194fa216d8f54489e15a7188d47dc",
|
|
2478
|
+
note: "Ver.6(2026-10-04〜05 描画前の掃除 sanitize.ts と共通の設定「外部参照」externalRefs、load.ts の derivedOf(無効な行と空の計算式は formula / usedFields を持たない)を入れた後。API に sanitizeReportCss / sanitizeReportNodes / isAllowedReportUrl / EXTERNAL_REFS / externalRefsOf を追加。途中の API 9cc7daf8… / b22564da… / 6c2e447d… は配っていないので載せない)"
|
|
2479
|
+
}
|
|
2480
|
+
]
|
|
2481
|
+
};
|
|
2482
|
+
var KNOWN_ENGINE_SHA256 = Object.fromEntries(Object.entries(KNOWN_PLUGIN_RELEASES).map(([v, rs]) => [v, rs.map((r) => r.engine)]));
|
|
2483
|
+
var REQUIRED_API = {
|
|
2484
|
+
apiVersion: "number",
|
|
2485
|
+
pluginVersion: "string",
|
|
2486
|
+
pluginId: "string",
|
|
2487
|
+
CONFIG_SCHEMA: "object",
|
|
2488
|
+
CONFIG_LIMITS: "object",
|
|
2489
|
+
PAGE_SIZES: "object",
|
|
2490
|
+
DPI_OPTIONS: "object",
|
|
2491
|
+
PAPER_NAMES: "object",
|
|
2492
|
+
PRINT_MODES: "object",
|
|
2493
|
+
buildMenuRows: "function",
|
|
2494
|
+
normalizeTagsInfo: "function",
|
|
2495
|
+
normalizeCssRows: "function",
|
|
2496
|
+
toSavedRows: "function",
|
|
2497
|
+
computeUsage: "function",
|
|
2498
|
+
computePluginUOG: "function",
|
|
2499
|
+
createCheckRecord: "function",
|
|
2500
|
+
createFieldsInfo: "function",
|
|
2501
|
+
isTargetType: "function",
|
|
2502
|
+
stripComments: "function",
|
|
2503
|
+
defaultCssRows: "function",
|
|
2504
|
+
defaultMenuInfo: "function",
|
|
2505
|
+
expandFields: "function",
|
|
2506
|
+
buildReportCss: "function",
|
|
2507
|
+
fileNameOf: "function",
|
|
2508
|
+
formulaOf: "function",
|
|
2509
|
+
mountReportHtml: "function",
|
|
2510
|
+
replaceTags: "function",
|
|
2511
|
+
getPaperSize: "function",
|
|
2512
|
+
webFontOf: "function",
|
|
2513
|
+
DUMMY_IMAGE: "string",
|
|
2514
|
+
validate: "function",
|
|
2515
|
+
parseJsonSafely: "function",
|
|
2516
|
+
ValidationError: "function",
|
|
2517
|
+
writeConfig: "function",
|
|
2518
|
+
DEFAULT_LIMITS: "object",
|
|
2519
|
+
ConfigStoreError: "function",
|
|
2520
|
+
// pull(アプリのプラグインの設定を読んで封筒形式にする。2026-10-05)
|
|
2521
|
+
readConfig: "function",
|
|
2522
|
+
buildExportData: "function"
|
|
2523
|
+
};
|
|
2524
|
+
function stableJson(v) {
|
|
2525
|
+
return JSON.stringify(v, (_k, x) => x instanceof RegExp ? `/${x.source}/${x.flags}` : x);
|
|
2526
|
+
}
|
|
2527
|
+
function sha256Hex2(data) {
|
|
2528
|
+
return createHash3("sha256").update(data).digest("hex");
|
|
2529
|
+
}
|
|
2530
|
+
function schemaRevisionOf(schema) {
|
|
2531
|
+
return sha256Hex2(stableJson(schema)).slice(0, 12);
|
|
2532
|
+
}
|
|
2533
|
+
function gitCommit(dir) {
|
|
2534
|
+
try {
|
|
2535
|
+
const head = execFileSync("git", ["-C", dir, "rev-parse", "--short", "HEAD"], { encoding: "utf8" }).trim();
|
|
2536
|
+
const dirty = execFileSync("git", ["-C", dir, "status", "--porcelain", "--untracked-files=no"], { encoding: "utf8" }).trim() !== "";
|
|
2537
|
+
return dirty ? `${head}+dirty` : head;
|
|
2538
|
+
} catch {
|
|
2539
|
+
return "unknown";
|
|
2540
|
+
}
|
|
2541
|
+
}
|
|
2542
|
+
function devMeta() {
|
|
2543
|
+
const pkg = JSON.parse(readFileSync7(path8.join(AUTHORING_ROOT, "package.json"), "utf8"));
|
|
2544
|
+
return { toolsVersion: pkg.version, supportedPluginVersions: SUPPORTED_PLUGIN_VERSIONS, supportedApiVersion: SUPPORTED_API_VERSION, commit: gitCommit(AUTHORING_ROOT), builtAt: "", mode: "dev" };
|
|
2545
|
+
}
|
|
2546
|
+
function toolsMeta() {
|
|
2547
|
+
if (true) return JSON.parse('{"toolsVersion":"0.1.0","supportedPluginVersions":["6"],"supportedApiVersion":1,"commit":"bce9598","builtAt":"2026-10-05T23:24:14.023Z","mode":"build"}');
|
|
2548
|
+
return devMeta();
|
|
2549
|
+
}
|
|
2550
|
+
|
|
2551
|
+
// src/engine.ts
|
|
2552
|
+
var DEFAULT_CONTEXT_BASE_URL = "https://example.cybozu.com";
|
|
2553
|
+
var DEFAULT_USER = { id: "1", code: "authoring", name: "authoring", email: "", language: "ja" };
|
|
2554
|
+
var loaded = null;
|
|
2555
|
+
function resolvePluginSources(opt = {}, mode = opt.mode ?? toolsMeta().mode) {
|
|
2556
|
+
const zip = opt.pluginZip ?? process.env.PCRAFT_PLUGIN_ZIP;
|
|
2557
|
+
if (zip) {
|
|
2558
|
+
if (!existsSync6(zip)) throw new PluginZipError(`印刷屋の zip が無い: ${zip}(.env の PCRAFT_PLUGIN_ZIP)`);
|
|
2559
|
+
return { ...readPluginZip(zip), kind: "zip" };
|
|
2560
|
+
}
|
|
2561
|
+
const devAllowed = mode === "dev" && process.env.PCRAFT_ALLOW_DEV_PLUGIN === "1";
|
|
2562
|
+
if (!devAllowed) {
|
|
2563
|
+
throw new PluginZipError(`印刷屋の zip の場所が分からない。.env に PCRAFT_PLUGIN_ZIP=<印刷屋プラグインの zip のパス> を書く${mode === "dev" ? "(開発中に隣の print-craft の prod/ を読むなら環境変数 PCRAFT_ALLOW_DEV_PLUGIN=1)" : ""}`);
|
|
2564
|
+
}
|
|
2565
|
+
const dev = devPluginDir();
|
|
2566
|
+
if (!dev) throw new PluginZipError("印刷屋の zip の場所が分からない(PCRAFT_ALLOW_DEV_PLUGIN=1 だが隣の print-craft の prod/ が無い)。.env に PCRAFT_PLUGIN_ZIP を書く");
|
|
2567
|
+
const read = (rel) => readFileSync8(path9.join(dev, rel), "utf8");
|
|
2568
|
+
const apiFile = path9.join(dev, API_ENTRY);
|
|
2569
|
+
const engine = read(ENGINE_ENTRY);
|
|
2570
|
+
const bignumber = read(BIGNUMBER_ENTRY);
|
|
2571
|
+
const momentTimezone = read(MOMENT_TZ_ENTRY);
|
|
2572
|
+
const api = existsSync6(apiFile) ? readFileSync8(apiFile, "utf8") : void 0;
|
|
2573
|
+
const manifest = JSON.parse(read(MANIFEST_ENTRY));
|
|
2574
|
+
return {
|
|
2575
|
+
kind: "dev",
|
|
2576
|
+
from: dev,
|
|
2577
|
+
manifest,
|
|
2578
|
+
pluginVersion: String(manifest.version ?? ""),
|
|
2579
|
+
engine,
|
|
2580
|
+
bignumber,
|
|
2581
|
+
momentTimezone,
|
|
2582
|
+
api,
|
|
2583
|
+
sha256: { engine: sha256Hex(engine), ...api ? { api: sha256Hex(api) } : {}, bignumber: sha256Hex(bignumber), momentTimezone: sha256Hex(momentTimezone) }
|
|
2584
|
+
};
|
|
2585
|
+
}
|
|
2586
|
+
var PARTS = [
|
|
2587
|
+
["計算式エンジン", "engine"],
|
|
2588
|
+
["authoring API", "api"],
|
|
2589
|
+
["bignumber", "bignumber"],
|
|
2590
|
+
["moment-timezone", "momentTimezone"]
|
|
2591
|
+
];
|
|
2592
|
+
function unknownParts(src, releases = KNOWN_PLUGIN_RELEASES) {
|
|
2593
|
+
const list = releases[src.pluginVersion];
|
|
2594
|
+
if (!list?.length) return ["版の一覧が無い"];
|
|
2595
|
+
const matches = (r) => PARTS.filter(([, k]) => src.sha256[k] && r[k] === src.sha256[k]).length;
|
|
2596
|
+
if (list.some((r) => matches(r) === PARTS.length)) return [];
|
|
2597
|
+
const show2 = ([label, k]) => `${label} ${src.sha256[k] ? src.sha256[k].slice(0, 12) + "…" : "無し"}`;
|
|
2598
|
+
const bestScore = Math.max(...list.map(matches));
|
|
2599
|
+
if (bestScore === 0) return PARTS.map(show2);
|
|
2600
|
+
const candidates = list.filter((r) => matches(r) === bestScore);
|
|
2601
|
+
const diffs = candidates.map((r) => PARTS.filter(([, k]) => !src.sha256[k] || r[k] !== src.sha256[k]).map(([label]) => label).join("|"));
|
|
2602
|
+
if (new Set(diffs).size !== 1) return PARTS.map(show2);
|
|
2603
|
+
const best = candidates[0];
|
|
2604
|
+
const diff2 = PARTS.filter(([, k]) => !src.sha256[k] || best[k] !== src.sha256[k]).map(show2);
|
|
2605
|
+
return diff2.length ? diff2 : PARTS.map(show2);
|
|
2606
|
+
}
|
|
2607
|
+
async function loadEngine(opt = {}) {
|
|
2608
|
+
if (loaded) return loaded;
|
|
2609
|
+
const meta = toolsMeta();
|
|
2610
|
+
const src = resolvePluginSources(opt, opt.mode ?? meta.mode);
|
|
2611
|
+
const warnings = [];
|
|
2612
|
+
if (!SUPPORTED_PLUGIN_VERSIONS.includes(src.pluginVersion)) {
|
|
2613
|
+
throw new PluginZipError(`印刷屋プラグインの版 ${src.pluginVersion || "不明"}(${src.from})には対応していない。tools が対応する版: ${SUPPORTED_PLUGIN_VERSIONS.join(", ")}`);
|
|
2614
|
+
}
|
|
2615
|
+
if (!src.api) throw new PluginZipError(`印刷屋の zip に ${API_ENTRY} が無い(Ver.6 以降の zip が要る): ${src.from}`);
|
|
2616
|
+
const unknown = unknownParts(src);
|
|
2617
|
+
const engineKnown = unknown.length === 0;
|
|
2618
|
+
if (!engineKnown) {
|
|
2619
|
+
const head = `印刷屋の zip の中身が tools の既知のリリースと違う: ${unknown.join("、")}(${src.from})`;
|
|
2620
|
+
if (src.kind === "dev") warnings.push(`${head}。開発中の print-craft(PCRAFT_ALLOW_DEV_PLUGIN=1)なので続ける`);
|
|
2621
|
+
else if (opt.allowUnknown) warnings.push(`${head}。PCRAFT_ALLOW_UNKNOWN_PLUGIN=1 なので続ける(結果が設定画面と違うことがある)`);
|
|
2622
|
+
else throw new PluginZipError(`${head}。配布元から取り直した zip を使うか、tools を新しい版にする。新しい修正版の zip だと分かっていて続けるなら、利用者が .env に PCRAFT_ALLOW_UNKNOWN_PLUGIN=1 を書く(zip の中のコードはこの PC の権限で動く。AI は .env を書けない)`);
|
|
2623
|
+
}
|
|
2624
|
+
const g = globalThis;
|
|
2625
|
+
const { Window } = await import("happy-dom");
|
|
2626
|
+
const ctx = { baseUrl: DEFAULT_CONTEXT_BASE_URL, appId: 1, loginUser: { ...DEFAULT_USER } };
|
|
2627
|
+
const window = new Window({ url: `${ctx.baseUrl}/k/${ctx.appId}/show` });
|
|
2628
|
+
for (const k of ["document", "Node", "Element", "HTMLElement", "HTMLDivElement", "HTMLImageElement", "HTMLIFrameElement", "DOMParser", "Event", "CSS", "getComputedStyle", "Blob", "File", "URL", "DOMRect", "requestAnimationFrame", "cancelAnimationFrame"]) {
|
|
2629
|
+
const v = window[k];
|
|
2630
|
+
if (v !== void 0 && g[k] === void 0) g[k] = k === "getComputedStyle" && typeof v === "function" ? v.bind(window) : v;
|
|
2631
|
+
}
|
|
2632
|
+
g.window = window;
|
|
2633
|
+
const kintone = {
|
|
2634
|
+
getLoginUser: () => ({ ...ctx.loginUser, isGuest: false }),
|
|
2635
|
+
app: { getId: () => ctx.appId },
|
|
2636
|
+
mobile: { app: { getId: () => ctx.appId } },
|
|
2637
|
+
api: Object.assign(() => Promise.reject(new Error("kintone.api is not available in authoring tools")), {
|
|
2638
|
+
url: (p, _guest) => `${ctx.baseUrl}${p.startsWith("/") ? "" : "/"}${p}`
|
|
2639
|
+
}),
|
|
2640
|
+
$PLUGIN_ID: "authoring"
|
|
2641
|
+
};
|
|
2642
|
+
g.kintone = kintone;
|
|
2643
|
+
g.cybozu = { data: { IS_MOBILE_DEVICE: false } };
|
|
2644
|
+
const u = ctx.loginUser;
|
|
2645
|
+
const infoUser = { id: u.id, code: u.code, name: u.name, email: u.email, primaryOrganization: null, organizations: [], groups: [] };
|
|
2646
|
+
const usersInfo = {
|
|
2647
|
+
users: [infoUser],
|
|
2648
|
+
organizations: [],
|
|
2649
|
+
groups: [],
|
|
2650
|
+
userCodes: { [u.code]: infoUser },
|
|
2651
|
+
orgCodes: {},
|
|
2652
|
+
groupCodes: {},
|
|
2653
|
+
userIds: { [u.id]: u.code },
|
|
2654
|
+
orgIds: {},
|
|
2655
|
+
groupIds: {}
|
|
2656
|
+
};
|
|
2657
|
+
g.rex0220_users_info3 = { request: () => void 0, getUsersInfo: async () => usersInfo, data: usersInfo };
|
|
2658
|
+
window.kintone = g.kintone;
|
|
2659
|
+
window.cybozu = g.cybozu;
|
|
2660
|
+
window.rex0220_users_info3 = g.rex0220_users_info3;
|
|
2661
|
+
if (!existsSync6(MOMENT_FILE)) throw new Error(`moment が無い: ${MOMENT_FILE}(tools の npm install 時に取る。手で取るなら npm run vendor)`);
|
|
2662
|
+
const runScript = (code, filename) => {
|
|
2663
|
+
vm.runInThisContext(code, { filename });
|
|
2664
|
+
for (const k of ["moment", "rex0220_BigNumber", "rex0220_Decimal", "rex0220p", "rex0220PrintCraftAuthoring"]) {
|
|
2665
|
+
if (g[k] === void 0 && window[k] !== void 0) g[k] = window[k];
|
|
2666
|
+
if (window[k] === void 0 && g[k] !== void 0) window[k] = g[k];
|
|
2667
|
+
}
|
|
2668
|
+
};
|
|
2669
|
+
runScript(readFileSync8(MOMENT_FILE, "utf8"), MOMENT_FILE);
|
|
2670
|
+
runScript(src.momentTimezone, `${src.from}!${MOMENT_TZ_ENTRY}`);
|
|
2671
|
+
runScript(src.bignumber, `${src.from}!${BIGNUMBER_ENTRY}`);
|
|
2672
|
+
runScript(src.engine, `${src.from}!${ENGINE_ENTRY}`);
|
|
2673
|
+
runScript(src.api, `${src.from}!${API_ENTRY}`);
|
|
2674
|
+
const rex0220p = window.rex0220p;
|
|
2675
|
+
const Ctor = rex0220p?.KintoneFormulaPCraft;
|
|
2676
|
+
if (typeof Ctor !== "function") throw new Error("KintoneFormulaPCraft is not loaded");
|
|
2677
|
+
const api = window.rex0220PrintCraftAuthoring;
|
|
2678
|
+
if (!api || typeof api !== "object") throw new PluginZipError("印刷屋の authoring API(rex0220PrintCraftAuthoring)が読めない");
|
|
2679
|
+
if (api.apiVersion !== SUPPORTED_API_VERSION) throw new PluginZipError(`印刷屋の authoring API の版 ${String(api.apiVersion)} には対応していない(tools は ${SUPPORTED_API_VERSION})。tools を印刷屋の版に合わせて更新する`);
|
|
2680
|
+
if (String(api.pluginVersion) !== src.pluginVersion) throw new PluginZipError(`authoring API の印刷屋の版 ${String(api.pluginVersion)} が zip の manifest の版 ${src.pluginVersion} と違う(組み替えられた zip)`);
|
|
2681
|
+
const bad = Object.entries(REQUIRED_API).filter(([k, t]) => typeof api[k] !== t).map(([k]) => k);
|
|
2682
|
+
if (bad.length) throw new PluginZipError(`印刷屋の authoring API に tools が使うものが無い、または型が違う: ${bad.join(", ")}(tools と印刷屋の版を合わせる)`);
|
|
2683
|
+
loaded = {
|
|
2684
|
+
window,
|
|
2685
|
+
Ctor,
|
|
2686
|
+
api,
|
|
2687
|
+
source: { kind: src.kind, from: src.from, pluginVersion: src.pluginVersion, engineSha256: src.sha256.engine, apiSha256: src.sha256.api, engineKnown, ...src.pluginId ? { pluginId: src.pluginId } : {} },
|
|
2688
|
+
warnings,
|
|
2689
|
+
setContext(next) {
|
|
2690
|
+
if (next.baseUrl) ctx.baseUrl = next.baseUrl.replace(/\/+$/, "");
|
|
2691
|
+
if (next.appId) ctx.appId = next.appId;
|
|
2692
|
+
if (next.loginUser) ctx.loginUser = { ...ctx.loginUser, ...next.loginUser };
|
|
2693
|
+
},
|
|
2694
|
+
checker: (pp, crec) => new Ctor("formula", pp, crec, true, true),
|
|
2695
|
+
runner: (pp, record2) => new Ctor("formula", pp, record2, true),
|
|
2696
|
+
functionNames: () => Object.keys(new Ctor("formula", {}, {}, true, true).funs).sort()
|
|
2697
|
+
};
|
|
2698
|
+
return loaded;
|
|
2699
|
+
}
|
|
2700
|
+
|
|
2701
|
+
// src/preview/render.ts
|
|
2702
|
+
var PREVIEW_CSP = "default-src 'none'; img-src data:; style-src 'unsafe-inline'; font-src data:; connect-src 'none'; frame-src 'none'; object-src 'none'; base-uri 'none'; form-action 'none'";
|
|
2703
|
+
var OUTER_CSP = "script-src 'none'; object-src 'none'; base-uri 'none'; form-action 'none'; style-src 'unsafe-inline'; img-src data:; connect-src 'none'";
|
|
2704
|
+
function fontOrigins(cssUrl) {
|
|
2705
|
+
let origin;
|
|
2706
|
+
try {
|
|
2707
|
+
const u = new URL(cssUrl);
|
|
2708
|
+
if (u.protocol !== "https:") return null;
|
|
2709
|
+
origin = u.origin;
|
|
2710
|
+
} catch {
|
|
2711
|
+
return null;
|
|
2712
|
+
}
|
|
2713
|
+
const font = [origin];
|
|
2714
|
+
if (origin === "https://fonts.googleapis.com") font.push("https://fonts.gstatic.com");
|
|
2715
|
+
return { style: [origin], font };
|
|
2716
|
+
}
|
|
2717
|
+
function previewCsp(font) {
|
|
2718
|
+
const o = font ? fontOrigins(font.cssUrl) : null;
|
|
2719
|
+
if (!o) return PREVIEW_CSP;
|
|
2720
|
+
return `default-src 'none'; img-src data:; style-src 'unsafe-inline' ${o.style.join(" ")}; font-src data: ${o.font.join(" ")}; connect-src 'none'; frame-src 'none'; object-src 'none'; base-uri 'none'; form-action 'none'`;
|
|
2721
|
+
}
|
|
2722
|
+
var PREVIEW_REMOVE_SELECTOR = "script,meta,link,base,form,input,button,select,textarea,iframe,frame,object,embed,applet,noscript,template,video,audio,source,track,canvas,map,area,animate,animatemotion,animatetransform,animatecolor,set,foreignobject,math";
|
|
2723
|
+
var REMOVE_ATTRS = /* @__PURE__ */ new Set(["srcdoc", "formaction", "action", "ping", "target", "download", "background", "poster", "manifest"]);
|
|
2724
|
+
var PAGES_CSS = `body { margin: 0; background: #f7f7f7; }
|
|
2725
|
+
.xp-rex0220-print-craft-overlay-pages { display: block; width: fit-content; font-size: 16px; line-height: 1.5; font-family: "メイリオ", Meiryo, "Hiragino Kaku Gothic ProN", "ヒラギノ角ゴ ProN W3", "MS Pゴシック", "Lucida Grande", "Lucida Sans Unicode", Arial, Verdana, sans-serif; -webkit-text-size-adjust: 100%; }
|
|
2726
|
+
.pcraft-authoring-error { color: #b00020; background: #fde7e9; border: 1px solid #b00020; border-radius: 3px; padding: 0 4px; font-size: 12px; font-family: monospace; }`;
|
|
2727
|
+
function escapeHtml(s) {
|
|
2728
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
2729
|
+
}
|
|
2730
|
+
function escapeCssText(css) {
|
|
2731
|
+
return css.replace(/</g, "\\3c ").replace(/-->/g, "--\\3e ");
|
|
2732
|
+
}
|
|
2733
|
+
function errorSpan(expression, e) {
|
|
2734
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
2735
|
+
return `<span class="pcraft-authoring-error">式のエラー: ${escapeHtml(expression)} — ${escapeHtml(message)}</span>`;
|
|
2736
|
+
}
|
|
2737
|
+
function renderRows(api, row, record2, kf, errors) {
|
|
2738
|
+
const rows = row.tagsInfo?.fieldsInfo ?? [];
|
|
2739
|
+
let html = "";
|
|
2740
|
+
for (let index = 2; index < rows.length; index++) {
|
|
2741
|
+
const info = rows[index];
|
|
2742
|
+
if (!info.state) continue;
|
|
2743
|
+
let tmp = info.html ? info.html.replace(/\$\{(.*?)\}/g, (_m, key) => {
|
|
2744
|
+
try {
|
|
2745
|
+
const v = kf.dq(key);
|
|
2746
|
+
return v == null ? "" : String(v);
|
|
2747
|
+
} catch (e) {
|
|
2748
|
+
errors.push(`HTML 設定 ${index + 1} 行目 \${${key}}: ${e instanceof Error ? e.message : String(e)}`);
|
|
2749
|
+
return errorSpan(`\${${key}}`, e);
|
|
2750
|
+
}
|
|
2751
|
+
}) : "";
|
|
2752
|
+
const formula = api.formulaOf(info);
|
|
2753
|
+
if (formula) {
|
|
2754
|
+
record2.$html = { value: tmp, type: "MULTI_LINE_TEXT" };
|
|
2755
|
+
try {
|
|
2756
|
+
const v = kf.dq(formula);
|
|
2757
|
+
tmp = v == null ? "" : String(v);
|
|
2758
|
+
} catch (e) {
|
|
2759
|
+
errors.push(`HTML 設定 ${index + 1} 行目の計算式: ${e instanceof Error ? e.message : String(e)}`);
|
|
2760
|
+
tmp += errorSpan(formula.length > 120 ? formula.slice(0, 120) + "…" : formula, e);
|
|
2761
|
+
}
|
|
2762
|
+
}
|
|
2763
|
+
html += tmp;
|
|
2764
|
+
}
|
|
2765
|
+
return html.replace(/\\/g, "¥");
|
|
2766
|
+
}
|
|
2767
|
+
function sanitizePreviewDom(host) {
|
|
2768
|
+
let removedElements = 0;
|
|
2769
|
+
let removedAttributes = 0;
|
|
2770
|
+
for (const el of Array.from(host.querySelectorAll(PREVIEW_REMOVE_SELECTOR))) {
|
|
2771
|
+
el.remove();
|
|
2772
|
+
removedElements++;
|
|
2773
|
+
}
|
|
2774
|
+
for (const el of Array.from(host.querySelectorAll("*"))) {
|
|
2775
|
+
for (const a of Array.from(el.attributes)) {
|
|
2776
|
+
const name = a.name.toLowerCase();
|
|
2777
|
+
const value = String(a.value ?? "").replace(/[\t\n\r]/g, "").trim();
|
|
2778
|
+
const isLink = name === "href" || name === "xlink:href";
|
|
2779
|
+
if (name.startsWith("on") || REMOVE_ATTRS.has(name) || isLink && !value.startsWith("#") || /^[\u0000- ]*javascript:/i.test(value)) {
|
|
2780
|
+
el.removeAttribute(a.name);
|
|
2781
|
+
removedAttributes++;
|
|
2782
|
+
}
|
|
2783
|
+
}
|
|
2784
|
+
if (el.tagName.toLowerCase() === "style") el.textContent = escapeCssText(el.textContent ?? "");
|
|
2785
|
+
}
|
|
2786
|
+
return { removedElements, removedAttributes };
|
|
2787
|
+
}
|
|
2788
|
+
function renderButton(input) {
|
|
2789
|
+
const { row, model, engine, body } = input;
|
|
2790
|
+
const api = model.api;
|
|
2791
|
+
const tags = row.tagsInfo;
|
|
2792
|
+
if (!tags) throw new Error(`${row.menu}: HTML 設定が無い`);
|
|
2793
|
+
const errors = [];
|
|
2794
|
+
const record2 = JSON.parse(JSON.stringify(input.record));
|
|
2795
|
+
const paper = api.getPaperSize(tags.pageSize || "A4", tags.orientation || "p", Number(tags.dpi) || 96);
|
|
2796
|
+
const kf = engine.runner(model.ppRun, record2);
|
|
2797
|
+
const fileName = api.fileNameOf(row, kf);
|
|
2798
|
+
const font = api.webFontOf(body.fontInfo);
|
|
2799
|
+
const cssBody = Array.isArray(body.cssInfo) ? body : { ...body, cssInfo: api.defaultCssRows() };
|
|
2800
|
+
const css = api.buildReportCss(cssBody, row, paper.scr, font);
|
|
2801
|
+
const html = renderRows(api, row, record2, kf, errors);
|
|
2802
|
+
const doc = engine.window.document;
|
|
2803
|
+
const host = doc.createElement("div");
|
|
2804
|
+
const pages = api.mountReportHtml(host, html, { sanitize: body.externalRefs !== "allow" });
|
|
2805
|
+
api.replaceTags(pages, {}, {}, api.DUMMY_IMAGE);
|
|
2806
|
+
if (pages.length === 0) errors.push('ページ要素(class="rex0220-pcraft-page")が無い。印刷屋は 0 ページの PDF を作る');
|
|
2807
|
+
sanitizePreviewDom(host);
|
|
2808
|
+
const wf = input.webFont ?? null;
|
|
2809
|
+
const useFont = wf && wf.approved && fontOrigins(wf.font.cssUrl) ? wf.font : null;
|
|
2810
|
+
const fontLink = useFont ? `
|
|
2811
|
+
<link rel="stylesheet" href="${escapeHtml(useFont.cssUrl)}">` : "";
|
|
2812
|
+
const inner = `<!doctype html>
|
|
2813
|
+
<html lang="ja"><head><meta charset="utf-8">
|
|
2814
|
+
<meta http-equiv="Content-Security-Policy" content="${previewCsp(useFont)}">${fontLink}
|
|
2815
|
+
<title>${escapeHtml(row.menu)}</title>
|
|
2816
|
+
<style>${PAGES_CSS}</style>
|
|
2817
|
+
<style class="xp-rex0220-print-craft-page-style">${escapeCssText(css)}</style>
|
|
2818
|
+
</head><body><div class="xp-rex0220-print-craft-overlay-pages">${host.innerHTML}</div></body></html>`;
|
|
2819
|
+
const width = paper.scr.width + 40;
|
|
2820
|
+
const height = Math.max(1, pages.length) * (paper.scr.height + 40) + 40;
|
|
2821
|
+
const errorList = errors.length ? `<ul class="errors">${errors.map((e) => `<li>${escapeHtml(e)}</li>`).join("")}</ul>` : "";
|
|
2822
|
+
const fontNote = useFont ? `Web フォント「${escapeHtml(useFont.family)}」は ${escapeHtml(fontOrigins(useFont.cssUrl).style[0])} から読みます(承認済み。preview の外部通信はこれだけ)。` : wf ? `Web フォント「${escapeHtml(wf.font.family)}」は配信元が未承認のため読みません(OS の書体で代替。利用者が policy/authoring-policy.json の allowExternal に書けば読みます)。` : "Web フォントは使いません。";
|
|
2823
|
+
const outer = `<!doctype html>
|
|
2824
|
+
<html lang="ja"><head><meta charset="utf-8">
|
|
2825
|
+
<meta http-equiv="Content-Security-Policy" content="${OUTER_CSP}">
|
|
2826
|
+
<title>preview: ${escapeHtml(row.menu)}</title>
|
|
2827
|
+
<style>
|
|
2828
|
+
body { margin: 0; font-family: system-ui, sans-serif; background: #e9ecef; color: #222; }
|
|
2829
|
+
header { padding: 12px 16px; background: #fff; border-bottom: 1px solid #ccc; }
|
|
2830
|
+
h1 { margin: 0 0 4px; font-size: 18px; }
|
|
2831
|
+
header p { margin: 2px 0; font-size: 13px; color: #555; }
|
|
2832
|
+
.errors { margin: 8px 0 0; padding: 8px 12px 8px 28px; background: #fde7e9; border: 1px solid #b00020; color: #b00020; font-size: 13px; }
|
|
2833
|
+
iframe { display: block; margin: 12px auto; border: 0; background: #f7f7f7; }
|
|
2834
|
+
</style></head><body>
|
|
2835
|
+
<header>
|
|
2836
|
+
<h1>${escapeHtml(row.menu)}</h1>
|
|
2837
|
+
<p>ファイル名: ${escapeHtml(fileName)} / 用紙: ${escapeHtml(tags.pageSize)} ${tags.orientation === "l" ? "横" : "縦"} ${escapeHtml(String(tags.dpi))} dpi / ページ: ${pages.length}</p>
|
|
2838
|
+
<p>これは近似のプレビューです。添付ファイルの画像と QR はダミーです。${fontNote} PDF の見た目は印刷屋プラグインで確かめてください。帳票は sandbox の iframe の中に置き、スクリプトと外部への通信(承認した Web フォントの配信元を除く)は CSP と sandbox で止め、リンクと埋め込みの要素は外してあります(文字だけ残る)。</p>
|
|
2839
|
+
${errorList}
|
|
2840
|
+
</header>
|
|
2841
|
+
<iframe sandbox="" title="${escapeHtml(row.menu)}" width="${width}" height="${height}" srcdoc="${escapeHtml(inner)}"></iframe>
|
|
2842
|
+
</body></html>
|
|
2843
|
+
`;
|
|
2844
|
+
return { menu: row.menu, fileName, pageSize: tags.pageSize, orientation: tags.orientation, dpi: String(tags.dpi), pages: pages.length, errors, webFont: useFont ? useFont.cssUrl : null, inner, html: outer };
|
|
2845
|
+
}
|
|
2846
|
+
|
|
2847
|
+
// src/commands/preview.ts
|
|
2848
|
+
function extractRecord(file) {
|
|
2849
|
+
if (!file || typeof file !== "object") throw new Error("レコードの JSON がオブジェクトでない");
|
|
2850
|
+
const o = file;
|
|
2851
|
+
if (o.record && typeof o.record === "object") return o.record;
|
|
2852
|
+
const values = Object.values(o);
|
|
2853
|
+
if (values.length && values.every((v) => v && typeof v === "object" && "type" in v && "value" in v)) return o;
|
|
2854
|
+
throw new Error("レコードの JSON の形が分からない(record コマンドの出力か、/k/v1/record の応答か、{ 項目: { type, value } } の形)");
|
|
2855
|
+
}
|
|
2856
|
+
function previewFileNames(rows) {
|
|
2857
|
+
const used = /* @__PURE__ */ new Map();
|
|
2858
|
+
return rows.map((row, i) => {
|
|
2859
|
+
const base = safeFileName(row.menu, `button-${i + 1}`);
|
|
2860
|
+
const n = used.get(base.toLowerCase()) ?? 0;
|
|
2861
|
+
used.set(base.toLowerCase(), n + 1);
|
|
2862
|
+
return n ? `${base}-${n + 1}.html` : `${base}.html`;
|
|
2863
|
+
});
|
|
2864
|
+
}
|
|
2865
|
+
async function runPreview(input) {
|
|
2866
|
+
const normalized = await normalizeSettings({ settingsText: input.settingsText, settingsFile: input.settingsFile, fields: input.fields, engine: input.engine, policy: input.policy, baseUrl: input.baseUrl });
|
|
2867
|
+
const findings = normalized.findings;
|
|
2868
|
+
const results = [];
|
|
2869
|
+
const skipped = [];
|
|
2870
|
+
if (!normalized.body) return { findings, results, skipped, summary: normalized.summary };
|
|
2871
|
+
const record2 = extractRecord(input.recordFile);
|
|
2872
|
+
const missing = missingInRecord(normalized.body, input.fields, record2);
|
|
2873
|
+
if (missing.length) findings.warning("preview.record", "レコード", `設定が使う項目がプレビューのレコードに無い: ${missing.slice(0, 10).join(", ")}${missing.length > 10 ? ` 他 ${missing.length - 10}` : ""}(帳票では空になる。record --fields-from <この設定> で取り直す)`);
|
|
2874
|
+
const model = buildModel(input.fields, input.engine.api, input.baseUrl);
|
|
2875
|
+
input.engine.setContext({ baseUrl: model.baseUrl || DEFAULT_CONTEXT_BASE_URL, appId: input.fields.appId });
|
|
2876
|
+
const rows = normalized.body.pluginInfos ?? [];
|
|
2877
|
+
const targets = rows.filter((row) => row.state && row.tagsInfo && (input.button === void 0 || row.menu === input.button));
|
|
2878
|
+
const names = previewFileNames(targets);
|
|
2879
|
+
const font = input.engine.api.webFontOf(normalized.body.fontInfo);
|
|
2880
|
+
const webFont = font ? { font: { family: font.family, cssUrl: font.cssUrl }, approved: isAllowed(input.policy ?? { allowExternal: [] }, font.cssUrl, input.settingsFile) } : null;
|
|
2881
|
+
targets.forEach((row, i) => {
|
|
2882
|
+
if (row.list) {
|
|
2883
|
+
skipped.push(`${row.menu}: 一覧帳票は 1 レコードのプレビューの対象外(段階 2)`);
|
|
2884
|
+
return;
|
|
2885
|
+
}
|
|
2886
|
+
const r = renderButton({ body: normalized.body, row, model, engine: input.engine, record: record2, webFont });
|
|
2887
|
+
results.push({ ...r, file: names[i] });
|
|
2888
|
+
});
|
|
2889
|
+
if (input.button !== void 0 && results.length === 0 && !skipped.length) findings.error("preview.button", input.button, "その名前の有効なボタンが無い");
|
|
2890
|
+
const summary = `${normalized.summary}。プレビュー ${results.length} 件${skipped.length ? `、対象外 ${skipped.length}` : ""}、式のエラー ${results.reduce((n, r) => n + r.errors.length, 0)}`;
|
|
2891
|
+
return { findings, results, skipped, summary };
|
|
2892
|
+
}
|
|
2893
|
+
|
|
2894
|
+
// src/cli.ts
|
|
2895
|
+
var USAGE = `使い方: pcraft-authoring <command> [options]
|
|
2896
|
+
|
|
2897
|
+
version [--expect <印刷屋の版>] [--json]
|
|
2898
|
+
tools の版と、印刷屋の zip から読んだプラグインの版・authoring API の版・計算式エンジンの SHA-256・設定スキーマの版。
|
|
2899
|
+
zip が読めない、中身が既知と違う、対応しない版、または --expect の版と zip の版が違えば終了コード 1。
|
|
2900
|
+
fields --app N [--lang ja] [--preview] [--guest <spaceId>] [--out fields/<file>]
|
|
2901
|
+
項目定義とレイアウトとアプリ名を fields/<N>.json に保存(既定は運用中の形。--preview は設定画面と同じ preview の API)。
|
|
2902
|
+
fields --app N --summary
|
|
2903
|
+
取得済みの fields/<N>.json を 1 項目 1 行で(レイアウトの順。型・ラベル・書式・単位・選択肢・ルックアップ)。通信しない。
|
|
2904
|
+
pull --app N [--preview] [--guest <spaceId>] [--out settings/<file>] [--force] [--plugin-id <ID>]
|
|
2905
|
+
アプリに入っている印刷屋の今の設定を取って、設定画面の「設定をダウンロード」と同じ封筒形式で settings/<アプリ名>.json に保存(GET だけ)。
|
|
2906
|
+
kintone の API ラボ「アプリに追加されているプラグインの設定情報を取得する」を有効にした環境だけ。権限は運用中の設定がレコード閲覧+追加、
|
|
2907
|
+
--preview(動作テスト環境 = 保存して未反映の設定)がアプリ管理。プラグイン ID は印刷屋の zip から。既にあるファイルは --force で上書き。
|
|
2908
|
+
record --app N --id R [--fields-from <settings.json>] [--guest <spaceId>] [--out records/<file>]
|
|
2909
|
+
レコードを records/<N>-<R>.json に保存。--fields-from で設定が使う項目だけ残す。
|
|
2910
|
+
record --app N --id R --summary
|
|
2911
|
+
取得済みの records/<N>-<R>.json を 1 項目 1 行で。値は出さず形だけ(文字数・行数・数値の桁・テーブルの行数・添付の件数と種類)。通信しない。
|
|
2912
|
+
buttons <settings.json> [--button <名前>]
|
|
2913
|
+
設定のボタン一覧(画面・保存先・用紙・表示条件・ファイル名・帳票の行・更新項目)。--button でそのボタンの HTML / CSS / 計算式(data: の URL は長さだけ)。
|
|
2914
|
+
normalize <settings.json> --fields <fields.json> [--out settings/<file>] [--dry-run] [--check] [--json]
|
|
2915
|
+
設定画面と同じ手順で派生値を作り直し、検査して、エラーが無ければ書き戻す(既定は同じファイルに上書き。--dry-run は書かない)。
|
|
2916
|
+
--check は入力の派生値と生成した値の差を出す。外部 URL の承認は policy/authoring-policy.json(場所は固定。利用者が書く)。
|
|
2917
|
+
diff <before.json> <after.json> [--derived]
|
|
2918
|
+
既存設定の変更をインポートする前に人が見る差分(ボタン単位。派生値は --derived で含める)。
|
|
2919
|
+
preview <settings.json> --fields <fields.json> --record <record.json> [--button <名前>] [--out-dir out/<dir>] [--json]
|
|
2920
|
+
有効なボタンごとに帳票の HTML を out/<ボタン名>.html に書く(sandbox の iframe + CSP。画像はダミー。Web フォントは配信元が承認済みのときだけ読む)。
|
|
2921
|
+
一覧帳票は対象外。式の失敗は赤字で埋めて終了コード 1。
|
|
2922
|
+
|
|
2923
|
+
環境とアプリのフォルダー(作業フォルダーに environments.json があるとき。構成 1 = 開発と本番でドメインが違う、構成 2 = 同じドメインで開発用と本番のアプリ):
|
|
2924
|
+
全部のコマンドに --env <環境の名前>(省略は environments.json の default)。--app は番号か apps の名前。
|
|
2925
|
+
保存先は kintone/<ホスト名>/<番号>-<アプリ名>/(fields.json、records/<番号>.json、out/、設定のダウンロードと pull は
|
|
2926
|
+
rex0220-print-craft-app<番号>-<日時>.json の名前のまま)。認証はその環境の envFile だけ(OS の KINTONE_* は読まない)。
|
|
2927
|
+
normalize / preview にアプリのフォルダーの中のファイルを渡すと --fields は同じフォルダーの fields.json、preview --record 3 は records/3.json。
|
|
2928
|
+
ダウンロード / pull のファイルは書き換えない(edit で -edit.json に写して直す)。
|
|
2929
|
+
take [--env <名前>]
|
|
2930
|
+
inbox/ に置いた設定のダウンロードを、封筒の appId と environments.json の apps からアプリのフォルダーへ名前のまま移す。
|
|
2931
|
+
edit --app <アプリ> | edit <ダウンロードのファイル>
|
|
2932
|
+
一番新しい(または指定した)ダウンロード / pull を <名前>-edit.json に写す(既にあればそのまま)。直すのはこちら。
|
|
2933
|
+
files --app <アプリ>
|
|
2934
|
+
アプリのフォルダーのファイル(今の設定 = 一番新しいダウンロード / pull、-edit.json、新しい帳票、records、out)。
|
|
2935
|
+
buttons --app <アプリ>
|
|
2936
|
+
今の設定(一番新しいダウンロード / pull)のボタン一覧。
|
|
2937
|
+
|
|
2938
|
+
読むファイルは作業フォルダーの中、書き込み先は fields/ records/ settings/ temp/ out/ kintone/ の下だけ。
|
|
2939
|
+
.env(作業フォルダーのもの): KINTONE_BASE_URL(*.cybozu.com / *.kintone.com / *.cybozu.cn)と、KINTONE_API_TOKEN または KINTONE_USERNAME / KINTONE_PASSWORD
|
|
2940
|
+
(kintone 公式 MCP と同じ)、PCRAFT_PLUGIN_ZIP=<印刷屋プラグインの zip のパス>(計算式エンジンと印刷屋のコードをここから読む)。
|
|
2941
|
+
kintone には GET しか送らない。
|
|
2942
|
+
`;
|
|
2943
|
+
var FLAGS = /* @__PURE__ */ new Set(["preview", "check", "json", "dry-run", "derived", "summary", "force"]);
|
|
2944
|
+
var UsageError = class extends Error {
|
|
2945
|
+
};
|
|
2946
|
+
function option(args, name) {
|
|
2947
|
+
const i = args.indexOf(`--${name}`);
|
|
2948
|
+
return i >= 0 ? args[i + 1] : void 0;
|
|
2949
|
+
}
|
|
2950
|
+
function flag(args, name) {
|
|
2951
|
+
return args.includes(`--${name}`);
|
|
2952
|
+
}
|
|
2953
|
+
function positional(args) {
|
|
2954
|
+
const out = [];
|
|
2955
|
+
for (let i = 0; i < args.length; i++) {
|
|
2956
|
+
if (args[i].startsWith("--")) {
|
|
2957
|
+
if (!FLAGS.has(args[i].slice(2))) i++;
|
|
2958
|
+
continue;
|
|
2959
|
+
}
|
|
2960
|
+
out.push(args[i]);
|
|
2961
|
+
}
|
|
2962
|
+
return out;
|
|
2963
|
+
}
|
|
2964
|
+
function intOption(args, name, required) {
|
|
2965
|
+
const v = option(args, name);
|
|
2966
|
+
if (v === void 0) {
|
|
2967
|
+
if (required) throw new UsageError(`--${name} <数値> が要る`);
|
|
2968
|
+
return void 0;
|
|
2969
|
+
}
|
|
2970
|
+
if (!/^\d+$/.test(v)) throw new UsageError(`--${name} は数値: ${v}`);
|
|
2971
|
+
return Number(v);
|
|
2972
|
+
}
|
|
2973
|
+
function rejectRemovedOptions(args) {
|
|
2974
|
+
for (const name of ["policy", "plugin-zip"]) {
|
|
2975
|
+
if (args.includes(`--${name}`)) throw new UsageError(`--${name} は使えない。.env と policy/authoring-policy.json と PCRAFT_PLUGIN_ZIP は作業フォルダーのものに固定(AI が書けるファイルを読ませないため)`);
|
|
2976
|
+
}
|
|
2977
|
+
}
|
|
2978
|
+
function writeJson(file, data) {
|
|
2979
|
+
mkdirSync2(path10.dirname(file), { recursive: true });
|
|
2980
|
+
writeFileSync2(file, JSON.stringify(data, null, 2) + "\n", "utf8");
|
|
2981
|
+
}
|
|
2982
|
+
function writeText(file, text) {
|
|
2983
|
+
mkdirSync2(path10.dirname(file), { recursive: true });
|
|
2984
|
+
writeFileSync2(file, text, "utf8");
|
|
2985
|
+
}
|
|
2986
|
+
function shown(file) {
|
|
2987
|
+
const rel = path10.relative(process.cwd(), file);
|
|
2988
|
+
return rel && !rel.startsWith("..") ? rel : file;
|
|
2989
|
+
}
|
|
2990
|
+
async function engineFor() {
|
|
2991
|
+
const engine = await loadEngine({ pluginZip: pluginZipPath(), allowUnknown: allowUnknownPlugin() });
|
|
2992
|
+
for (const w of engine.warnings) console.error(`注意: ${w}`);
|
|
2993
|
+
return engine;
|
|
2994
|
+
}
|
|
2995
|
+
async function version(args) {
|
|
2996
|
+
const meta = toolsMeta();
|
|
2997
|
+
let engine = null;
|
|
2998
|
+
let error;
|
|
2999
|
+
try {
|
|
3000
|
+
engine = await engineFor();
|
|
3001
|
+
} catch (e) {
|
|
3002
|
+
error = e instanceof Error ? e.message : String(e);
|
|
3003
|
+
}
|
|
3004
|
+
const expect = option(args, "expect");
|
|
3005
|
+
if (expect !== void 0 && !expect) throw new UsageError("--expect には印刷屋の版(例: 6)を続ける");
|
|
3006
|
+
let expectError;
|
|
3007
|
+
if (expect !== void 0 && engine) {
|
|
3008
|
+
if (!meta.supportedPluginVersions.includes(expect)) expectError = `--expect ${expect}: tools ${meta.toolsVersion} が対応する印刷屋の版は ${meta.supportedPluginVersions.join(", ")}(tools を更新する)`;
|
|
3009
|
+
else if (engine.source.pluginVersion !== expect) expectError = `--expect ${expect}: zip の印刷屋の版は ${engine.source.pluginVersion}(${engine.source.from}。アプリに入れた版の zip を PCRAFT_PLUGIN_ZIP に書く)`;
|
|
3010
|
+
}
|
|
3011
|
+
const info = {
|
|
3012
|
+
...meta,
|
|
3013
|
+
plugin: engine ? { source: engine.source.kind, from: engine.source.from, pluginVersion: engine.source.pluginVersion, apiVersion: engine.api.apiVersion, engineSha256: engine.source.engineSha256, apiSha256: engine.source.apiSha256, engineKnown: engine.source.engineKnown, schemaRevision: schemaRevisionOf(engine.api.CONFIG_SCHEMA) } : null,
|
|
3014
|
+
expected: expect,
|
|
3015
|
+
ok: !!engine && !expectError,
|
|
3016
|
+
error: error ?? expectError
|
|
3017
|
+
};
|
|
3018
|
+
if (flag(args, "json")) {
|
|
3019
|
+
console.log(JSON.stringify(info, null, 2));
|
|
3020
|
+
} else {
|
|
3021
|
+
console.log(`@rex0220/print-craft-authoring-tools ${meta.toolsVersion}${meta.mode === "dev" ? "(開発中)" : ""}(対応する印刷屋の版 ${meta.supportedPluginVersions.join(", ")}、API ${meta.supportedApiVersion}。commit ${meta.commit}${meta.builtAt ? `、ビルド ${meta.builtAt}` : ""})`);
|
|
3022
|
+
if (engine) {
|
|
3023
|
+
console.log(`印刷屋プラグイン: 版 ${engine.source.pluginVersion}、authoring API ${engine.api.apiVersion}(${engine.source.kind === "zip" ? "zip" : "開発中の print-craft"}: ${engine.source.from})`);
|
|
3024
|
+
console.log(`計算式エンジン: sha256 ${engine.source.engineSha256}${engine.source.engineKnown ? "(zip の中身は既知)" : "(既知の一覧と違う中身を含む)"}`);
|
|
3025
|
+
console.log(`設定スキーマの版: ${schemaRevisionOf(engine.api.CONFIG_SCHEMA)}`);
|
|
3026
|
+
if (expectError) console.error(expectError);
|
|
3027
|
+
else if (expect !== void 0) console.log(`--expect ${expect}: 一致`);
|
|
3028
|
+
} else {
|
|
3029
|
+
console.error(`印刷屋の zip: ${error}`);
|
|
3030
|
+
}
|
|
3031
|
+
}
|
|
3032
|
+
return info.ok ? 0 : 1;
|
|
3033
|
+
}
|
|
3034
|
+
function context(args) {
|
|
3035
|
+
const ws = loadWorkspace();
|
|
3036
|
+
const name = option(args, "env");
|
|
3037
|
+
if (args.includes("--env")) {
|
|
3038
|
+
if (!name || !isEnvName(name)) throw new UsageError("--env には environments.json の環境の名前を続ける(ファイルの場所は指定できない)");
|
|
3039
|
+
if (!ws) throw new UsageError("--env は作業フォルダーに environments.json があるときだけ使える");
|
|
3040
|
+
}
|
|
3041
|
+
return ws ? { ws, env: pickEnv(ws, name) } : { ws: null };
|
|
3042
|
+
}
|
|
3043
|
+
function client(ctx) {
|
|
3044
|
+
const auth = ctx?.ws && ctx.env ? loadAuthForEnv(ctx.env) : loadAuth();
|
|
3045
|
+
return { client: createRestClient(auth), authLabel: describeAuth(auth) };
|
|
3046
|
+
}
|
|
3047
|
+
var envFlag = (ctx) => ctx.ws && ctx.env && ctx.env.name !== ctx.ws.defaultEnv ? ` --env ${ctx.env.name}` : "";
|
|
3048
|
+
function appOf(args, ctx) {
|
|
3049
|
+
if (!ctx.ws || !ctx.env) return intOption(args, "app", true);
|
|
3050
|
+
const v = option(args, "app");
|
|
3051
|
+
if (!v) throw new UsageError("--app <番号か、environments.json の apps の名前> が要る");
|
|
3052
|
+
return resolveApp(ctx.ws, ctx.env, v).appId;
|
|
3053
|
+
}
|
|
3054
|
+
function existingAppDir(ctx, appId) {
|
|
3055
|
+
const dir = findAppDir(process.cwd(), ctx.env, appId);
|
|
3056
|
+
if (!dir) throw new InputError(`アプリ ${appId} のフォルダー(kintone/${ctx.env.host}/${appId}-…)が無い。先に npx pcraft-authoring fields --app ${appId}${envFlag(ctx)}`);
|
|
3057
|
+
return dir;
|
|
3058
|
+
}
|
|
3059
|
+
function noOutInWorkspace(args, ctx) {
|
|
3060
|
+
if (ctx.ws && args.includes("--out")) throw new UsageError("environments.json があるときは --out を使わない(保存先はアプリのフォルダー kintone/<ホスト名>/<番号>-<アプリ名>/)");
|
|
3061
|
+
}
|
|
3062
|
+
function existingFile(rel, howToFetch) {
|
|
3063
|
+
const file = resolveRead(rel);
|
|
3064
|
+
if (!existsSync7(file)) throw new InputError(`${shown(file)} が無い。先に npx pcraft-authoring ${howToFetch}`);
|
|
3065
|
+
return file;
|
|
3066
|
+
}
|
|
3067
|
+
function folderDefaults(settingsFile) {
|
|
3068
|
+
return loadWorkspace() ? appFolderOfFile(process.cwd(), settingsFile) : null;
|
|
3069
|
+
}
|
|
3070
|
+
function fieldsFileOf(args, folder) {
|
|
3071
|
+
const arg = option(args, "fields");
|
|
3072
|
+
if (arg) return resolveRead(arg);
|
|
3073
|
+
if (!folder) throw new UsageError("--fields <fields.json> が要る(fields コマンドの出力。kintone/<ホスト名>/<番号>-…/ の中のファイルなら同じフォルダーの fields.json を使う)");
|
|
3074
|
+
return existingFile(path10.join(folder.dir, "fields.json"), `fields --app ${folder.appId}`);
|
|
3075
|
+
}
|
|
3076
|
+
function trustedBaseUrl(folder) {
|
|
3077
|
+
const ws = loadWorkspace();
|
|
3078
|
+
if (ws && folder) return envsOfHost(ws, folder.host)[0]?.baseUrl;
|
|
3079
|
+
return baseUrlFromEnv();
|
|
3080
|
+
}
|
|
3081
|
+
async function fields(args) {
|
|
3082
|
+
const ctx = context(args);
|
|
3083
|
+
const app = appOf(args, ctx);
|
|
3084
|
+
noOutInWorkspace(args, ctx);
|
|
3085
|
+
if (flag(args, "summary")) {
|
|
3086
|
+
const rel = ctx.ws ? path10.join(existingAppDir(ctx, app), "fields.json") : path10.join("fields", `${app}.json`);
|
|
3087
|
+
console.log(listFields(await readFieldsFile(existingFile(rel, `fields --app ${app}${envFlag(ctx)}`))));
|
|
3088
|
+
return 0;
|
|
3089
|
+
}
|
|
3090
|
+
const guestSpaceId = intOption(args, "guest", false);
|
|
3091
|
+
const { client: c, authLabel } = client(ctx);
|
|
3092
|
+
const file = await fetchFields(c, { app, lang: option(args, "lang"), preview: flag(args, "preview"), guestSpaceId });
|
|
3093
|
+
const out = ctx.ws ? resolveWrite(path10.join(appDirFor(process.cwd(), ctx.env, app, file.appName), "fields.json"), WRITE_ROOTS.kintone) : resolveWrite(option(args, "out") ?? path10.join("fields", `${app}.json`), WRITE_ROOTS.fields);
|
|
3094
|
+
writeJson(out, file);
|
|
3095
|
+
console.log(`${summarizeFields(file)}
|
|
3096
|
+
${authLabel}で ${c.baseUrl} から取得 → ${shown(out)}`);
|
|
3097
|
+
return 0;
|
|
3098
|
+
}
|
|
3099
|
+
async function record(args) {
|
|
3100
|
+
const ctx = context(args);
|
|
3101
|
+
const app = appOf(args, ctx);
|
|
3102
|
+
const id = intOption(args, "id", true);
|
|
3103
|
+
noOutInWorkspace(args, ctx);
|
|
3104
|
+
const defaultRel = () => ctx.ws ? path10.join(existingAppDir(ctx, app), "records", `${id}.json`) : path10.join("records", `${app}-${id}.json`);
|
|
3105
|
+
if (flag(args, "summary")) {
|
|
3106
|
+
const file2 = existingFile(defaultRel(), `record --app ${app} --id ${id}${envFlag(ctx)}`);
|
|
3107
|
+
const data = readJsonLimited(file2);
|
|
3108
|
+
if (!data.record || typeof data.record !== "object") throw new InputError(`${shown(file2)} は record コマンドの出力ではない(record が無い)`);
|
|
3109
|
+
console.log(describeRecord(data));
|
|
3110
|
+
return 0;
|
|
3111
|
+
}
|
|
3112
|
+
const guestSpaceId = intOption(args, "guest", false);
|
|
3113
|
+
const out = resolveWrite(option(args, "out") ?? defaultRel(), WRITE_ROOTS.records);
|
|
3114
|
+
const from = option(args, "fields-from");
|
|
3115
|
+
let keep;
|
|
3116
|
+
if (from) {
|
|
3117
|
+
const settings = readJsonLimited(resolveRead(from));
|
|
3118
|
+
const codes = usedFieldCodes(settings);
|
|
3119
|
+
if (!codes) console.log(`${from} に使う項目の情報が無いので絞らない(normalize した設定なら usedFields から絞れる)`);
|
|
3120
|
+
else keep = codes;
|
|
3121
|
+
}
|
|
3122
|
+
const { client: c, authLabel } = client(ctx);
|
|
3123
|
+
const file = await fetchRecord(c, { app, id, guestSpaceId, keep });
|
|
3124
|
+
writeJson(out, file);
|
|
3125
|
+
console.log(`${summarizeRecord(file)}
|
|
3126
|
+
${authLabel}で ${c.baseUrl} から取得 → ${shown(out)}(個人情報を含む。コミットしない)`);
|
|
3127
|
+
return 0;
|
|
3128
|
+
}
|
|
3129
|
+
async function normalize(args) {
|
|
3130
|
+
const [settingsArg] = positional(args);
|
|
3131
|
+
if (!settingsArg) throw new UsageError("設定 JSON のパスが要る");
|
|
3132
|
+
const settingsFile = resolveRead(settingsArg);
|
|
3133
|
+
const folder = folderDefaults(settingsFile);
|
|
3134
|
+
const fieldsFile = fieldsFileOf(args, folder);
|
|
3135
|
+
const write = !flag(args, "dry-run");
|
|
3136
|
+
if (write && folder && !option(args, "out") && SNAPSHOT_RE.test(path10.basename(settingsFile))) {
|
|
3137
|
+
throw new UsageError(`ダウンロード / pull のファイルは書き換えない。npx pcraft-authoring edit ${shown(settingsFile)} で -edit.json に写してから直す(検査だけなら --dry-run)`);
|
|
3138
|
+
}
|
|
3139
|
+
const out = write ? resolveWrite(option(args, "out") ?? settingsFile, WRITE_ROOTS.settings) : void 0;
|
|
3140
|
+
const fieldsData = await readFieldsFile(fieldsFile);
|
|
3141
|
+
const baseUrl = trustedBaseUrl(folder);
|
|
3142
|
+
const engine = await engineFor();
|
|
3143
|
+
engine.setContext({ baseUrl: baseUrl ?? DEFAULT_CONTEXT_BASE_URL, appId: fieldsData.appId });
|
|
3144
|
+
const result = await normalizeSettings({
|
|
3145
|
+
settingsText: readTextLimited(settingsFile),
|
|
3146
|
+
settingsFile: relativeSettingsPath(settingsArg),
|
|
3147
|
+
fields: fieldsData,
|
|
3148
|
+
engine,
|
|
3149
|
+
policy: loadPolicy(),
|
|
3150
|
+
check: flag(args, "check"),
|
|
3151
|
+
baseUrl
|
|
3152
|
+
});
|
|
3153
|
+
if (flag(args, "json")) {
|
|
3154
|
+
console.log(JSON.stringify({ summary: result.summary, findings: result.findings.items, size: result.size, checkDiffs: result.checkDiffs, written: !!result.output && write }, null, 2));
|
|
3155
|
+
} else {
|
|
3156
|
+
console.log(`normalize: ${settingsArg}(${result.summary})`);
|
|
3157
|
+
console.log(result.findings.format());
|
|
3158
|
+
if (result.checkDiffs) {
|
|
3159
|
+
console.log(result.checkDiffs.length ? `--check: 入力の派生値と生成した値の差 ${result.checkDiffs.length} 件` : "--check: 入力の派生値と生成した値は一致");
|
|
3160
|
+
for (const d of result.checkDiffs.slice(0, 50)) console.log(` ${d}`);
|
|
3161
|
+
if (result.checkDiffs.length > 50) console.log(` … 他 ${result.checkDiffs.length - 50} 件`);
|
|
3162
|
+
}
|
|
3163
|
+
}
|
|
3164
|
+
if (!result.output) {
|
|
3165
|
+
if (!flag(args, "json")) console.error("エラーがあるので書き出さない");
|
|
3166
|
+
return 1;
|
|
3167
|
+
}
|
|
3168
|
+
if (out) {
|
|
3169
|
+
writeJson(out, result.output);
|
|
3170
|
+
if (!flag(args, "json")) console.log(`出力: ${shown(out)}(封筒の date を更新。派生値を生成)`);
|
|
3171
|
+
}
|
|
3172
|
+
return 0;
|
|
3173
|
+
}
|
|
3174
|
+
async function diff(args) {
|
|
3175
|
+
const [before, after] = positional(args);
|
|
3176
|
+
if (!before || !after) throw new UsageError("diff <before.json> <after.json>");
|
|
3177
|
+
const a = readJsonLimited(resolveRead(before));
|
|
3178
|
+
const b = readJsonLimited(resolveRead(after));
|
|
3179
|
+
console.log(diffSettings(a, b, { derived: flag(args, "derived") }));
|
|
3180
|
+
return 0;
|
|
3181
|
+
}
|
|
3182
|
+
async function pull(args) {
|
|
3183
|
+
const ctx = context(args);
|
|
3184
|
+
const app = appOf(args, ctx);
|
|
3185
|
+
noOutInWorkspace(args, ctx);
|
|
3186
|
+
const guestSpaceId = intOption(args, "guest", false);
|
|
3187
|
+
const preview2 = flag(args, "preview");
|
|
3188
|
+
const outArg = option(args, "out");
|
|
3189
|
+
if (args.includes("--out") && !outArg) throw new UsageError("--out には settings/ の下のファイル名を続ける");
|
|
3190
|
+
const engine = await engineFor();
|
|
3191
|
+
const pluginId = option(args, "plugin-id") ?? engine.source.pluginId;
|
|
3192
|
+
if (!pluginId) throw new InputError("プラグイン ID が分からない(印刷屋の zip に PUBKEY が無い)。--plugin-id <ID>(アプリの設定 → プラグインの一覧で確かめる)");
|
|
3193
|
+
const { client: c, authLabel } = client(ctx);
|
|
3194
|
+
let result;
|
|
3195
|
+
try {
|
|
3196
|
+
result = await pullSettings(c, engine, { app, preview: preview2, guestSpaceId, pluginId });
|
|
3197
|
+
} catch (e) {
|
|
3198
|
+
if (e instanceof RestError && /plugin\/config/.test(e.apiPath ?? "")) {
|
|
3199
|
+
throw new RestError(`${e.message}。確かめること: kintone の API ラボ「アプリに追加されているプラグインの設定情報を取得または更新するREST API」が有効か(cybozu.com 共通管理者がアップデートオプションの「検討中の新機能」で)、権限(運用中: レコード閲覧+レコード追加 / --preview: アプリ管理)、アプリに印刷屋プラグイン(ID ${pluginId})が入っているか`, e.status, e.code, e.apiPath);
|
|
3200
|
+
}
|
|
3201
|
+
throw e;
|
|
3202
|
+
}
|
|
3203
|
+
const out = ctx.ws ? resolveWrite(path10.join(appDirFor(process.cwd(), ctx.env, app, result.appName), snapshotNameOf(app)), WRITE_ROOTS.kintone) : resolveWrite(outArg ?? path10.join("settings", defaultPullName(result.appName, app)), WRITE_ROOTS.settings);
|
|
3204
|
+
if (existsSync7(out) && !flag(args, "force")) throw new InputError(`${shown(out)} は既にある。上書きするなら --force(上書きの前の内容は git の差分で確かめる)、別の名前なら --out settings/<ファイル>`);
|
|
3205
|
+
writeJson(out, result.envelope);
|
|
3206
|
+
console.log(`pull: アプリ ${app} ${result.appName}(${preview2 ? "動作テスト環境" : "運用中"}の設定、revision ${result.revision}、保存形式 ${result.format})を${authLabel}で ${c.baseUrl} から取得 → ${shown(out)}`);
|
|
3207
|
+
console.log(listButtons(result.envelope, { file: shown(out) }));
|
|
3208
|
+
return 0;
|
|
3209
|
+
}
|
|
3210
|
+
async function buttons(args) {
|
|
3211
|
+
let [settingsArg] = positional(args);
|
|
3212
|
+
if (!settingsArg && args.includes("--app")) {
|
|
3213
|
+
const ctx = context(args);
|
|
3214
|
+
if (!ctx.ws) throw new UsageError("buttons --app は environments.json があるときだけ。今までの形では buttons <設定 JSON>");
|
|
3215
|
+
const app = appOf(args, ctx);
|
|
3216
|
+
const list = listAppFolder(existingAppDir(ctx, app));
|
|
3217
|
+
if (!list.snapshots.length) throw new InputError(`${shown(list.dir)} にダウンロード / pull のファイルが無い(設定画面でダウンロードして inbox/ に置き take、または pull --app ${app}${envFlag(ctx)})`);
|
|
3218
|
+
settingsArg = shown(path10.join(list.dir, list.snapshots[0]));
|
|
3219
|
+
}
|
|
3220
|
+
if (!settingsArg) throw new UsageError("設定 JSON のパスが要る");
|
|
3221
|
+
const button = option(args, "button");
|
|
3222
|
+
if (args.includes("--button") && !button) throw new UsageError("--button にはボタン名を続ける");
|
|
3223
|
+
console.log(listButtons(readJsonLimited(resolveRead(settingsArg)), { file: settingsArg, button }));
|
|
3224
|
+
return 0;
|
|
3225
|
+
}
|
|
3226
|
+
async function take(args) {
|
|
3227
|
+
const ctx = context(args);
|
|
3228
|
+
if (!ctx.ws) throw new UsageError("take は environments.json があるときだけ使える(inbox/ のダウンロードをアプリのフォルダーへ移す)");
|
|
3229
|
+
const r = takeInbox(process.cwd(), ctx.ws, args.includes("--env") ? ctx.env.name : void 0);
|
|
3230
|
+
for (const m of r.moved) console.log(`${m.file} → ${m.to}${m.same ? "(同じものが既にあったので inbox から消した)" : ""}`);
|
|
3231
|
+
for (const s of r.skipped) console.error(`移さない: ${s.file}(${s.reason})`);
|
|
3232
|
+
if (!r.moved.length && !r.skipped.length) console.log(`${INBOX}/ に設定のファイルが無い`);
|
|
3233
|
+
return r.skipped.length ? 1 : 0;
|
|
3234
|
+
}
|
|
3235
|
+
async function edit(args) {
|
|
3236
|
+
const ctx = context(args);
|
|
3237
|
+
if (!ctx.ws) throw new UsageError("edit は environments.json があるときだけ使える(今までの形では settings/ のファイルをそのまま直す)");
|
|
3238
|
+
let [src] = positional(args);
|
|
3239
|
+
if (!src) {
|
|
3240
|
+
const app = appOf(args, ctx);
|
|
3241
|
+
const list = listAppFolder(existingAppDir(ctx, app));
|
|
3242
|
+
if (!list.snapshots.length) throw new InputError(`${shown(list.dir)} にダウンロード / pull のファイルが無い`);
|
|
3243
|
+
src = path10.join(list.dir, list.snapshots[0]);
|
|
3244
|
+
}
|
|
3245
|
+
const file = resolveRead(src);
|
|
3246
|
+
if (!SNAPSHOT_RE.test(path10.basename(file)) || !appFolderOfFile(process.cwd(), file)) throw new UsageError(`edit に渡すのはアプリのフォルダーのダウンロード / pull のファイル(rex0220-print-craft-app<番号>-<日時>.json): ${src}`);
|
|
3247
|
+
const dest = resolveWrite(editNameOf(file), WRITE_ROOTS.kintone);
|
|
3248
|
+
if (existsSync7(dest)) {
|
|
3249
|
+
console.log(`既にある(続けて直す): ${shown(dest)}`);
|
|
3250
|
+
} else {
|
|
3251
|
+
writeText(dest, readTextLimited(file));
|
|
3252
|
+
console.log(`${shown(file)} → ${shown(dest)}(ここを直す。ダウンロードのファイルは書き換えない)`);
|
|
3253
|
+
}
|
|
3254
|
+
console.log(`次: 直したら npx pcraft-authoring normalize ${shown(dest)} → diff ${shown(file)} ${shown(dest)}`);
|
|
3255
|
+
return 0;
|
|
3256
|
+
}
|
|
3257
|
+
async function files(args) {
|
|
3258
|
+
const ctx = context(args);
|
|
3259
|
+
if (!ctx.ws) throw new UsageError("files は environments.json があるときだけ使える");
|
|
3260
|
+
const app = appOf(args, ctx);
|
|
3261
|
+
const l = listAppFolder(existingAppDir(ctx, app));
|
|
3262
|
+
const lines = [
|
|
3263
|
+
`${shown(l.dir)}(環境 ${ctx.env.name}、${ctx.env.baseUrl})`,
|
|
3264
|
+
`fields.json: ${l.hasFields ? "あり" : `無い(npx pcraft-authoring fields --app ${app}${envFlag(ctx)})`}`,
|
|
3265
|
+
`ダウンロード / pull(新しい順。先頭が今の設定): ${l.snapshots.length ? "" : "無い"}`,
|
|
3266
|
+
...l.snapshots.map((n, i) => ` ${n}${i === 0 ? " ← 今の設定" : ""}`),
|
|
3267
|
+
`直したもの(-edit.json): ${l.edits.length ? "" : "無い"}`,
|
|
3268
|
+
...l.edits.map((n) => ` ${n}`),
|
|
3269
|
+
`その他の設定(新しい帳票など): ${l.reports.length ? "" : "無い"}`,
|
|
3270
|
+
...l.reports.map((n) => ` ${n}`),
|
|
3271
|
+
`records/: ${l.records.length ? l.records.join(", ") : "無い"} out/: ${l.outs} 件`
|
|
3272
|
+
];
|
|
3273
|
+
console.log(lines.join("\n"));
|
|
3274
|
+
return 0;
|
|
3275
|
+
}
|
|
3276
|
+
async function preview(args) {
|
|
3277
|
+
const [settingsArg] = positional(args);
|
|
3278
|
+
if (!settingsArg) throw new UsageError("設定 JSON のパスが要る");
|
|
3279
|
+
const settingsFile = resolveRead(settingsArg);
|
|
3280
|
+
const folder = folderDefaults(settingsFile);
|
|
3281
|
+
const fieldsFile = fieldsFileOf(args, folder);
|
|
3282
|
+
const recordArg = option(args, "record");
|
|
3283
|
+
if (!recordArg) throw new UsageError("--record <record.json> が要る(record コマンドの出力。アプリのフォルダーの中のファイルならレコード番号だけでよい: --record 3)");
|
|
3284
|
+
const recordFile = folder && /^\d+$/.test(recordArg) ? existingFile(path10.join(folder.dir, "records", `${recordArg}.json`), `record --app ${folder.appId} --id ${recordArg}`) : resolveRead(recordArg);
|
|
3285
|
+
const outDir = resolveWriteDir(option(args, "out-dir") ?? (folder ? path10.join(folder.dir, "out") : "out"), WRITE_ROOTS.out);
|
|
3286
|
+
const fieldsData = await readFieldsFile(fieldsFile);
|
|
3287
|
+
const engine = await engineFor();
|
|
3288
|
+
const result = await runPreview({
|
|
3289
|
+
settingsText: readTextLimited(settingsFile),
|
|
3290
|
+
settingsFile: relativeSettingsPath(settingsArg),
|
|
3291
|
+
fields: fieldsData,
|
|
3292
|
+
recordFile: readJsonLimited(recordFile),
|
|
3293
|
+
engine,
|
|
3294
|
+
policy: loadPolicy(),
|
|
3295
|
+
button: option(args, "button"),
|
|
3296
|
+
baseUrl: trustedBaseUrl(folder)
|
|
3297
|
+
});
|
|
3298
|
+
const written = [];
|
|
3299
|
+
if (!result.findings.hasErrors) {
|
|
3300
|
+
for (const r of result.results) {
|
|
3301
|
+
const file = resolveWrite(path10.join(outDir, r.file), WRITE_ROOTS.out);
|
|
3302
|
+
writeText(file, r.html);
|
|
3303
|
+
written.push(shown(file));
|
|
3304
|
+
}
|
|
3305
|
+
}
|
|
3306
|
+
if (flag(args, "json")) {
|
|
3307
|
+
console.log(JSON.stringify({ summary: result.summary, findings: result.findings.items, results: result.results.map((r) => ({ menu: r.menu, file: shown(path10.join(outDir, r.file)), fileName: r.fileName, pages: r.pages, errors: r.errors })), skipped: result.skipped, written }, null, 2));
|
|
3308
|
+
} else {
|
|
3309
|
+
console.log(`preview: ${settingsArg}(${result.summary})`);
|
|
3310
|
+
console.log(result.findings.format());
|
|
3311
|
+
for (const r of result.results) {
|
|
3312
|
+
console.log(`${r.menu}: ${r.pages} ページ、ファイル名 ${r.fileName}${r.errors.length ? `、式のエラー ${r.errors.length}` : ""} → ${shown(path10.join(outDir, r.file))}`);
|
|
3313
|
+
for (const e of r.errors) console.log(` ${e}`);
|
|
3314
|
+
}
|
|
3315
|
+
for (const s of result.skipped) console.log(`対象外: ${s}`);
|
|
3316
|
+
if (result.findings.hasErrors) console.error("設定にエラーがあるのでプレビューは書かない(normalize で直す)");
|
|
3317
|
+
else if (written.length) console.log(`出力 ${written.length} 件(レコードの値を含む。コミットしない。Chrome で開く)`);
|
|
3318
|
+
}
|
|
3319
|
+
return result.findings.hasErrors || result.results.some((r) => r.errors.length) ? 1 : 0;
|
|
3320
|
+
}
|
|
3321
|
+
async function main(argv) {
|
|
3322
|
+
const [command, ...args] = argv;
|
|
3323
|
+
rejectRemovedOptions(args);
|
|
3324
|
+
if (args.includes("--env")) context(args);
|
|
3325
|
+
switch (command) {
|
|
3326
|
+
case void 0:
|
|
3327
|
+
case "help":
|
|
3328
|
+
case "--help":
|
|
3329
|
+
case "-h":
|
|
3330
|
+
console.log(USAGE);
|
|
3331
|
+
return command === void 0 ? 2 : 0;
|
|
3332
|
+
case "version":
|
|
3333
|
+
return version(args);
|
|
3334
|
+
case "fields":
|
|
3335
|
+
return fields(args);
|
|
3336
|
+
case "record":
|
|
3337
|
+
return record(args);
|
|
3338
|
+
case "normalize":
|
|
3339
|
+
return normalize(args);
|
|
3340
|
+
case "diff":
|
|
3341
|
+
return diff(args);
|
|
3342
|
+
case "buttons":
|
|
3343
|
+
return buttons(args);
|
|
3344
|
+
case "pull":
|
|
3345
|
+
return pull(args);
|
|
3346
|
+
case "take":
|
|
3347
|
+
return take(args);
|
|
3348
|
+
case "edit":
|
|
3349
|
+
return edit(args);
|
|
3350
|
+
case "files":
|
|
3351
|
+
return files(args);
|
|
3352
|
+
case "preview":
|
|
3353
|
+
return preview(args);
|
|
3354
|
+
default:
|
|
3355
|
+
console.error(`不明なコマンド: ${command}
|
|
3356
|
+
${USAGE}`);
|
|
3357
|
+
return 2;
|
|
3358
|
+
}
|
|
3359
|
+
}
|
|
3360
|
+
main(process.argv.slice(2)).then(
|
|
3361
|
+
(code) => {
|
|
3362
|
+
process.exitCode = code;
|
|
3363
|
+
},
|
|
3364
|
+
(e) => {
|
|
3365
|
+
if (e instanceof UsageError || e instanceof PathError) {
|
|
3366
|
+
console.error(`${e.message}
|
|
3367
|
+
${USAGE}`);
|
|
3368
|
+
process.exitCode = 2;
|
|
3369
|
+
} else if (e instanceof AuthError || e instanceof RestError || e instanceof NotAllowedError || e instanceof PluginZipError || e instanceof PolicyError || e instanceof InputError || e instanceof KintoneUrlError || e instanceof ButtonNotFoundError || e instanceof WorkspaceError) {
|
|
3370
|
+
console.error(e.message);
|
|
3371
|
+
process.exitCode = 1;
|
|
3372
|
+
} else {
|
|
3373
|
+
console.error(e instanceof Error ? e.message : String(e));
|
|
3374
|
+
process.exitCode = 1;
|
|
3375
|
+
}
|
|
3376
|
+
}
|
|
3377
|
+
);
|