@decocms/blocks-cli 7.8.0 → 7.9.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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@decocms/blocks-cli",
|
|
3
|
-
"version": "7.
|
|
3
|
+
"version": "7.9.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Deco codegen (generate-blocks, generate-schema, generate-invoke) and Fresh-to-TanStack migration tooling",
|
|
6
6
|
"repository": {
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
},
|
|
21
21
|
"exports": {
|
|
22
22
|
"./generate-blocks": "./scripts/generate-blocks.ts",
|
|
23
|
+
"./generate-blocks-manifest": "./scripts/generate-blocks-manifest.ts",
|
|
23
24
|
"./generate-schema": "./scripts/generate-schema.ts",
|
|
24
25
|
"./generate-invoke": "./scripts/generate-invoke.ts",
|
|
25
26
|
"./migrate": "./scripts/migrate.ts",
|
|
@@ -34,7 +35,7 @@
|
|
|
34
35
|
"lint:unused": "knip"
|
|
35
36
|
},
|
|
36
37
|
"dependencies": {
|
|
37
|
-
"@decocms/blocks": "7.
|
|
38
|
+
"@decocms/blocks": "7.9.0",
|
|
38
39
|
"ts-morph": "^27.0.0",
|
|
39
40
|
"tsx": "^4.22.5"
|
|
40
41
|
},
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Integration test for `generate-blocks-manifest.ts`.
|
|
3
|
+
*
|
|
4
|
+
* Drives the programmatic entry against a tmp fixture of hostile-but-real
|
|
5
|
+
* block filenames (the shapes found in production `.deco/blocks` corpora:
|
|
6
|
+
* double-encoded `%2520`, single-encoded `%20`, parentheses, encoded slashes
|
|
7
|
+
* `%2F`, raw UTF-8), then verifies the emitted module the way a site would
|
|
8
|
+
* consume it: keys verbatim, `tsc` accepts it, and importing it (via a tsx
|
|
9
|
+
* child process from the fixture dir, resolving the raw-filename JSON import
|
|
10
|
+
* specifiers against the real files on disk) yields the parsed contents.
|
|
11
|
+
*/
|
|
12
|
+
import * as cp from "node:child_process";
|
|
13
|
+
import * as fs from "node:fs";
|
|
14
|
+
import * as os from "node:os";
|
|
15
|
+
import * as path from "node:path";
|
|
16
|
+
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
17
|
+
import { generateBlocksManifest } from "./generate-blocks-manifest";
|
|
18
|
+
|
|
19
|
+
const SCRIPT = path.resolve(__dirname, "generate-blocks-manifest.ts");
|
|
20
|
+
|
|
21
|
+
const HOSTILE_FIXTURE: Record<string, unknown> = {
|
|
22
|
+
"pages-PDP%2520Box-102215": { path: "/pdp-box", encoded: "double" },
|
|
23
|
+
"pages-Home%20(principal)-287364": { path: "/", parens: true },
|
|
24
|
+
"collections%2Fblog%2Fauthors%2Fx": { nested: "encoded-slash" },
|
|
25
|
+
"pages-Calçados-42": { utf8: "çãé" },
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
describe("generate-blocks-manifest", () => {
|
|
29
|
+
let tmpDir: string;
|
|
30
|
+
let blocksDir: string;
|
|
31
|
+
let outFile: string;
|
|
32
|
+
|
|
33
|
+
beforeEach(() => {
|
|
34
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "generate-blocks-manifest-"));
|
|
35
|
+
blocksDir = path.join(tmpDir, ".deco", "blocks");
|
|
36
|
+
outFile = path.join(tmpDir, ".deco", "blocksManifest.gen.ts");
|
|
37
|
+
fs.mkdirSync(blocksDir, { recursive: true });
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
afterEach(() => {
|
|
41
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
const writeFixture = (fixture: Record<string, unknown> = HOSTILE_FIXTURE) => {
|
|
45
|
+
for (const [key, value] of Object.entries(fixture)) {
|
|
46
|
+
fs.writeFileSync(path.join(blocksDir, `${key}.json`), JSON.stringify(value));
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
it("emits verbatim keys and raw-filename import specifiers for hostile names", async () => {
|
|
51
|
+
writeFixture();
|
|
52
|
+
const result = await generateBlocksManifest({ blocksDir, outFile, silent: true });
|
|
53
|
+
expect(result.count).toBe(4);
|
|
54
|
+
expect(result.empty).toBe(false);
|
|
55
|
+
expect(result.written).toBe(true);
|
|
56
|
+
|
|
57
|
+
const emitted = fs.readFileSync(outFile, "utf-8");
|
|
58
|
+
for (const key of Object.keys(HOSTILE_FIXTURE)) {
|
|
59
|
+
// Key: filename minus .json, verbatim — no decoding of %2520/%20/%2F.
|
|
60
|
+
expect(emitted).toContain(`${JSON.stringify(key)}: `);
|
|
61
|
+
// Specifier: the raw on-disk filename, relative to the emitted module.
|
|
62
|
+
expect(emitted).toContain(`from ${JSON.stringify(`./blocks/${key}.json`)};`);
|
|
63
|
+
}
|
|
64
|
+
// Never URL-decoded anywhere in the module.
|
|
65
|
+
expect(emitted).not.toContain("pages-PDP%20Box");
|
|
66
|
+
expect(emitted).not.toContain("pages-Home (principal)");
|
|
67
|
+
expect(emitted).not.toContain("collections/blog");
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("compiles under tsc (module esnext, bundler resolution, resolveJsonModule)", async () => {
|
|
71
|
+
writeFixture();
|
|
72
|
+
await generateBlocksManifest({ blocksDir, outFile, silent: true });
|
|
73
|
+
|
|
74
|
+
const r = cp.spawnSync(
|
|
75
|
+
"npx",
|
|
76
|
+
[
|
|
77
|
+
"tsc",
|
|
78
|
+
"--noEmit",
|
|
79
|
+
"--strict",
|
|
80
|
+
"--module",
|
|
81
|
+
"esnext",
|
|
82
|
+
"--moduleResolution",
|
|
83
|
+
"bundler",
|
|
84
|
+
"--target",
|
|
85
|
+
"es2022",
|
|
86
|
+
"--resolveJsonModule",
|
|
87
|
+
"--skipLibCheck",
|
|
88
|
+
outFile,
|
|
89
|
+
],
|
|
90
|
+
{ encoding: "utf8" },
|
|
91
|
+
);
|
|
92
|
+
expect(r.stdout + r.stderr).not.toMatch(/error TS/);
|
|
93
|
+
expect(r.status).toBe(0);
|
|
94
|
+
}, 30_000);
|
|
95
|
+
|
|
96
|
+
it("importing the emitted module (tsx, from the fixture dir) yields the parsed contents", async () => {
|
|
97
|
+
writeFixture();
|
|
98
|
+
await generateBlocksManifest({ blocksDir, outFile, silent: true });
|
|
99
|
+
|
|
100
|
+
const runner = path.join(tmpDir, ".deco", "runner.ts");
|
|
101
|
+
fs.writeFileSync(
|
|
102
|
+
runner,
|
|
103
|
+
'import blocks from "./blocksManifest.gen";\nconsole.log(JSON.stringify(blocks));\n',
|
|
104
|
+
);
|
|
105
|
+
const r = cp.spawnSync("npx", ["tsx", runner], { encoding: "utf8", cwd: tmpDir });
|
|
106
|
+
expect(r.stderr).toBe("");
|
|
107
|
+
expect(r.status).toBe(0);
|
|
108
|
+
expect(JSON.parse(r.stdout)).toEqual(HOSTILE_FIXTURE);
|
|
109
|
+
}, 30_000);
|
|
110
|
+
|
|
111
|
+
it("is idempotent: regeneration over an unchanged block set writes nothing", async () => {
|
|
112
|
+
writeFixture();
|
|
113
|
+
const first = await generateBlocksManifest({ blocksDir, outFile, silent: true });
|
|
114
|
+
expect(first.written).toBe(true);
|
|
115
|
+
const emittedFirst = fs.readFileSync(outFile, "utf-8");
|
|
116
|
+
|
|
117
|
+
const second = await generateBlocksManifest({ blocksDir, outFile, silent: true });
|
|
118
|
+
expect(second.written).toBe(false);
|
|
119
|
+
expect(fs.readFileSync(outFile, "utf-8")).toBe(emittedFirst);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it("orders imports deterministically regardless of readdir order (sorted filenames)", async () => {
|
|
123
|
+
writeFixture({ "z-last": { z: 1 }, "a-first": { a: 1 }, "m-mid": { m: 1 } });
|
|
124
|
+
await generateBlocksManifest({ blocksDir, outFile, silent: true });
|
|
125
|
+
|
|
126
|
+
const emitted = fs.readFileSync(outFile, "utf-8");
|
|
127
|
+
const importOrder = [...emitted.matchAll(/from "\.\/blocks\/([^"]+)\.json";/g)].map(
|
|
128
|
+
(m) => m[1],
|
|
129
|
+
);
|
|
130
|
+
expect(importOrder).toEqual(["a-first", "m-mid", "z-last"]);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it("ignores non-json entries and subdirectories (top-level *.json only)", async () => {
|
|
134
|
+
writeFixture({ real: { ok: true } });
|
|
135
|
+
fs.writeFileSync(path.join(blocksDir, "notes.txt"), "not a block");
|
|
136
|
+
fs.mkdirSync(path.join(blocksDir, "nested"));
|
|
137
|
+
fs.writeFileSync(path.join(blocksDir, "nested", "deep.json"), "{}");
|
|
138
|
+
|
|
139
|
+
const result = await generateBlocksManifest({ blocksDir, outFile, silent: true });
|
|
140
|
+
expect(result.count).toBe(1);
|
|
141
|
+
const emitted = fs.readFileSync(outFile, "utf-8");
|
|
142
|
+
expect(emitted).toContain('"real": _b0');
|
|
143
|
+
expect(emitted).not.toContain("notes.txt");
|
|
144
|
+
expect(emitted).not.toContain("deep.json");
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it("emits an empty manifest when the blocks dir is missing", async () => {
|
|
148
|
+
fs.rmSync(blocksDir, { recursive: true, force: true });
|
|
149
|
+
const result = await generateBlocksManifest({ blocksDir, outFile, silent: true });
|
|
150
|
+
expect(result.count).toBe(0);
|
|
151
|
+
expect(result.empty).toBe(true);
|
|
152
|
+
|
|
153
|
+
const emitted = fs.readFileSync(outFile, "utf-8");
|
|
154
|
+
expect(emitted).toContain("const blocks: Record<string, unknown> = {");
|
|
155
|
+
expect(emitted).toContain("export default blocks;");
|
|
156
|
+
expect(emitted).not.toContain("import _b");
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it("never lets a generated comment contain the block-comment terminator", async () => {
|
|
160
|
+
// Filenames land ONLY inside JSON.stringify-quoted string literals — a
|
|
161
|
+
// name containing `*` + `/` must not be able to truncate any comment in
|
|
162
|
+
// the emitted module (past incident with interpolated doc comments).
|
|
163
|
+
writeFixture({ "weird-*∕name": { ok: true }, "star*": { s: 1 } });
|
|
164
|
+
await generateBlocksManifest({ blocksDir, outFile, silent: true });
|
|
165
|
+
|
|
166
|
+
const emitted = fs.readFileSync(outFile, "utf-8");
|
|
167
|
+
const commentLines = emitted.split("\n").filter((l) => l.startsWith("//"));
|
|
168
|
+
for (const line of commentLines) {
|
|
169
|
+
expect(line).not.toContain("*/");
|
|
170
|
+
expect(line).not.toContain("weird");
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it("runs as a CLI with --blocks-dir/--out-file overrides", () => {
|
|
175
|
+
writeFixture({ "cli-block": { via: "cli" } });
|
|
176
|
+
const cliOut = path.join(tmpDir, "custom", "manifest.gen.ts");
|
|
177
|
+
|
|
178
|
+
const r = cp.spawnSync(
|
|
179
|
+
"npx",
|
|
180
|
+
["tsx", SCRIPT, "--blocks-dir", blocksDir, "--out-file", cliOut],
|
|
181
|
+
{ encoding: "utf8", cwd: tmpDir },
|
|
182
|
+
);
|
|
183
|
+
expect(r.status).toBe(0);
|
|
184
|
+
expect(r.stdout).toContain("Generated static-import manifest for 1 blocks");
|
|
185
|
+
|
|
186
|
+
const emitted = fs.readFileSync(cliOut, "utf-8");
|
|
187
|
+
expect(emitted).toContain('"cli-block": _b0');
|
|
188
|
+
// Specifier is relative to the out file's own directory.
|
|
189
|
+
expect(emitted).toContain('from "../.deco/blocks/cli-block.json";');
|
|
190
|
+
}, 30_000);
|
|
191
|
+
});
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
/**
|
|
3
|
+
* Reads .deco/blocks/*.json and emits blocksManifest.gen.ts — a module that
|
|
4
|
+
* STATICALLY imports every block file and re-exports them as one
|
|
5
|
+
* `Record<string, unknown>` keyed exactly like
|
|
6
|
+
* `@decocms/blocks/cms/loadDecofileDirectory`: filename minus the `.json`
|
|
7
|
+
* extension, verbatim (no URL-decoding, no renaming — the `pages-` filename
|
|
8
|
+
* prefix on page blocks is load-bearing, see loadDecofileDirectory's doc
|
|
9
|
+
* comment).
|
|
10
|
+
*
|
|
11
|
+
* Why static imports instead of the runtime fs read: a plain
|
|
12
|
+
* `loadDecofileDirectory(".deco/blocks")` is invisible to the bundler, so in
|
|
13
|
+
* `next dev` editing a block JSON invalidates nothing (no CMS content
|
|
14
|
+
* hot-reload) and production deploys need `outputFileTracingIncludes` hacks
|
|
15
|
+
* to ship the directory. With the manifest, Next's own module graph owns the
|
|
16
|
+
* files: editing an imported JSON re-evaluates the server module graph
|
|
17
|
+
* (~120–165ms measured), which also resets module-scope memos like
|
|
18
|
+
* `createNextSetup`'s bootstrap cache — content edits reload naturally, and
|
|
19
|
+
* the JSON is bundled into the build output. The trade-off: adding or
|
|
20
|
+
* removing a block FILE requires re-running this generator (content edits do
|
|
21
|
+
* not), so wire it into the site's `generate` chain.
|
|
22
|
+
*
|
|
23
|
+
* Import specifiers use the RAW on-disk filename. Verified against webpack,
|
|
24
|
+
* Turbopack, and Vite: specifiers are opaque strings to all three — `%2520`,
|
|
25
|
+
* `%20`, parentheses, `%C3%A7`, `%2F` etc. pass through verbatim and nothing
|
|
26
|
+
* URL-decodes, so JSON.stringify-quoting the specifier (and the key) is all
|
|
27
|
+
* the escaping needed.
|
|
28
|
+
*
|
|
29
|
+
* Usage (from site root):
|
|
30
|
+
* npx tsx node_modules/@decocms/blocks-cli/scripts/generate-blocks-manifest.ts
|
|
31
|
+
*
|
|
32
|
+
* CLI:
|
|
33
|
+
* --blocks-dir override input (default: .deco/blocks)
|
|
34
|
+
* --out-file override output (default: .deco/blocksManifest.gen.ts)
|
|
35
|
+
*
|
|
36
|
+
* Programmatic:
|
|
37
|
+
* import { generateBlocksManifest } from "@decocms/blocks-cli/generate-blocks-manifest";
|
|
38
|
+
* await generateBlocksManifest({ blocksDir, outFile });
|
|
39
|
+
*/
|
|
40
|
+
import fs from "node:fs";
|
|
41
|
+
import path from "node:path";
|
|
42
|
+
|
|
43
|
+
// Header of the emitted module. Kept as line comments and deliberately free
|
|
44
|
+
// of interpolated filenames: block filenames can contain almost any
|
|
45
|
+
// character, and a filename containing the sequence `*` + `/` inside a
|
|
46
|
+
// generated /** ... */ block would terminate the comment early and corrupt
|
|
47
|
+
// the module (a past incident). Filenames only ever appear inside
|
|
48
|
+
// JSON.stringify-quoted string literals below.
|
|
49
|
+
const HEADER = [
|
|
50
|
+
"// Auto-generated by @decocms/blocks-cli/scripts/generate-blocks-manifest.ts — do not edit.",
|
|
51
|
+
"//",
|
|
52
|
+
"// Static-import manifest of every JSON decofile in the blocks directory,",
|
|
53
|
+
"// keyed by filename minus the .json extension, VERBATIM — the same key",
|
|
54
|
+
"// format @decocms/blocks/cms/loadDecofileDirectory produces (page blocks",
|
|
55
|
+
"// keep their load-bearing `pages-` filename prefix).",
|
|
56
|
+
"//",
|
|
57
|
+
"// Because every block file is a static import, the bundler's module graph",
|
|
58
|
+
"// owns them: in `next dev`, editing a block JSON re-evaluates the server",
|
|
59
|
+
"// module graph (CMS content hot-reload), and production builds bundle the",
|
|
60
|
+
"// content (no outputFileTracingIncludes needed). Adding or removing a",
|
|
61
|
+
"// block FILE requires re-running the generator; editing content does not.",
|
|
62
|
+
"//",
|
|
63
|
+
"// Regenerate:",
|
|
64
|
+
"// npx tsx node_modules/@decocms/blocks-cli/scripts/generate-blocks-manifest.ts",
|
|
65
|
+
"",
|
|
66
|
+
].join("\n");
|
|
67
|
+
|
|
68
|
+
export interface GenerateBlocksManifestOptions {
|
|
69
|
+
blocksDir: string;
|
|
70
|
+
outFile: string;
|
|
71
|
+
/** Suppress the per-run summary log. Defaults to false. */
|
|
72
|
+
silent?: boolean;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export interface GenerateBlocksManifestResult {
|
|
76
|
+
count: number;
|
|
77
|
+
outFile: string;
|
|
78
|
+
/** True when the blocks dir was missing and an empty manifest was emitted. */
|
|
79
|
+
empty: boolean;
|
|
80
|
+
/**
|
|
81
|
+
* True when the manifest on disk actually changed. Regeneration is
|
|
82
|
+
* idempotent: an unchanged block set rewrites nothing, so watchers (and
|
|
83
|
+
* Next's module graph) are not tickled by no-op runs.
|
|
84
|
+
*/
|
|
85
|
+
written: boolean;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function renderManifest(blocksDir: string, outFile: string, files: string[]): string {
|
|
89
|
+
const lines: string[] = [HEADER];
|
|
90
|
+
|
|
91
|
+
const sorted = [...files].sort(); // code-unit sort — locale-independent, diff-stable
|
|
92
|
+
|
|
93
|
+
for (let i = 0; i < sorted.length; i++) {
|
|
94
|
+
// Import specifier: the RAW filename, relative to the emitted module.
|
|
95
|
+
// JSON.stringify provides all necessary escaping — bundlers do not
|
|
96
|
+
// URL-decode specifiers, so no percent-escaping/normalizing here.
|
|
97
|
+
let spec = path
|
|
98
|
+
.relative(path.dirname(outFile), path.join(blocksDir, sorted[i]))
|
|
99
|
+
.replace(/\\/g, "/");
|
|
100
|
+
if (!spec.startsWith(".")) spec = `./${spec}`;
|
|
101
|
+
lines.push(`import _b${i} from ${JSON.stringify(spec)};`);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
lines.push("");
|
|
105
|
+
lines.push("const blocks: Record<string, unknown> = {");
|
|
106
|
+
for (let i = 0; i < sorted.length; i++) {
|
|
107
|
+
const key = sorted[i].slice(0, -".json".length);
|
|
108
|
+
lines.push(` ${JSON.stringify(key)}: _b${i},`);
|
|
109
|
+
}
|
|
110
|
+
lines.push("};");
|
|
111
|
+
lines.push("");
|
|
112
|
+
lines.push("export default blocks;");
|
|
113
|
+
lines.push("");
|
|
114
|
+
|
|
115
|
+
return lines.join("\n");
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async function writeIfChanged(outFile: string, content: string): Promise<boolean> {
|
|
119
|
+
let existing: string | undefined;
|
|
120
|
+
try {
|
|
121
|
+
existing = await fs.promises.readFile(outFile, "utf-8");
|
|
122
|
+
} catch {}
|
|
123
|
+
if (existing === content) return false;
|
|
124
|
+
await fs.promises.mkdir(path.dirname(outFile), { recursive: true });
|
|
125
|
+
await fs.promises.writeFile(outFile, content);
|
|
126
|
+
return true;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export async function generateBlocksManifest(
|
|
130
|
+
options: GenerateBlocksManifestOptions,
|
|
131
|
+
): Promise<GenerateBlocksManifestResult> {
|
|
132
|
+
const blocksDir = path.resolve(options.blocksDir);
|
|
133
|
+
const outFile = path.resolve(options.outFile);
|
|
134
|
+
const silent = options.silent ?? false;
|
|
135
|
+
|
|
136
|
+
if (!fs.existsSync(blocksDir)) {
|
|
137
|
+
if (!silent) {
|
|
138
|
+
console.warn(`Blocks directory not found: ${blocksDir} — generating empty manifest.`);
|
|
139
|
+
}
|
|
140
|
+
const written = await writeIfChanged(outFile, renderManifest(blocksDir, outFile, []));
|
|
141
|
+
return { count: 0, outFile, empty: true, written };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Top-level *.json files only — mirrors loadDecofileDirectory, which
|
|
145
|
+
// filters `entry.isFile()` and never recurses (nested paths live encoded
|
|
146
|
+
// in the FILENAME, e.g. `collections%2Fblog%2F....json`).
|
|
147
|
+
const files = (await fs.promises.readdir(blocksDir, { withFileTypes: true }))
|
|
148
|
+
.filter((entry) => entry.isFile() && entry.name.endsWith(".json"))
|
|
149
|
+
.map((entry) => entry.name);
|
|
150
|
+
|
|
151
|
+
const written = await writeIfChanged(outFile, renderManifest(blocksDir, outFile, files));
|
|
152
|
+
|
|
153
|
+
if (!silent) {
|
|
154
|
+
console.log(
|
|
155
|
+
`Generated static-import manifest for ${files.length} blocks → ` +
|
|
156
|
+
`${path.relative(process.cwd(), outFile)}${written ? "" : " (unchanged)"}`,
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return { count: files.length, outFile, empty: false, written };
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// ---------------------------------------------------------------------------
|
|
164
|
+
// CLI shim — same pattern as generate-blocks.ts.
|
|
165
|
+
// ---------------------------------------------------------------------------
|
|
166
|
+
|
|
167
|
+
function isMainModule(): boolean {
|
|
168
|
+
// tsx/node ESM: import.meta.url matches process.argv[1] when invoked directly.
|
|
169
|
+
// Use a forgiving comparison so it works under both `tsx script.ts` and
|
|
170
|
+
// `node --import tsx script.ts`.
|
|
171
|
+
const entry = process.argv[1];
|
|
172
|
+
if (!entry) return false;
|
|
173
|
+
try {
|
|
174
|
+
const entryUrl = new URL(`file://${path.resolve(entry)}`).href;
|
|
175
|
+
return import.meta.url === entryUrl;
|
|
176
|
+
} catch {
|
|
177
|
+
return false;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (isMainModule()) {
|
|
182
|
+
const args = process.argv.slice(2);
|
|
183
|
+
const arg = (name: string, fallback: string): string => {
|
|
184
|
+
const idx = args.indexOf(`--${name}`);
|
|
185
|
+
return idx !== -1 && args[idx + 1] ? args[idx + 1] : fallback;
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
const blocksDir = path.resolve(process.cwd(), arg("blocks-dir", ".deco/blocks"));
|
|
189
|
+
const outFile = path.resolve(process.cwd(), arg("out-file", ".deco/blocksManifest.gen.ts"));
|
|
190
|
+
|
|
191
|
+
generateBlocksManifest({ blocksDir, outFile }).catch((err) => {
|
|
192
|
+
console.error(err);
|
|
193
|
+
process.exit(1);
|
|
194
|
+
});
|
|
195
|
+
}
|