Skip to content

Commit c9550bd

Browse files
committed
Improve Windows snippet rendering fallback
1 parent 6db8216 commit c9550bd

3 files changed

Lines changed: 132 additions & 30 deletions

File tree

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
"./method/station-criteria.json": "./src/data/method/station-criteria.json"
2727
},
2828
"scripts": {
29-
"test": "node scripts/validate.mjs",
29+
"test": "node scripts/validate.mjs && node scripts/test-print-method-snippet.mjs",
3030
"release:create-apiops:pack": "npm pack --workspace packages/create-apiops",
3131
"release:create-apiops:publish": "npm publish --workspace packages/create-apiops --access public",
3232
"check:packaging:skills": "node scripts/check-packaging-skills.mjs",
Lines changed: 81 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import fs from "fs";
22
import path from "path";
3+
import { spawnSync } from "child_process";
4+
import { fileURLToPath } from "url";
35

46
function readJson(filePath) {
57
return JSON.parse(fs.readFileSync(filePath, "utf8"));
@@ -29,7 +31,7 @@ function resolveSnippetPath(snippet, locale) {
2931
return fs.existsSync(localized) ? localized : path.join(base, snippet);
3032
}
3133

32-
function toAscii(text) {
34+
export function toAscii(text) {
3335
return text
3436
.replace(/\r\n/g, "\n")
3537
.replace(/\uFEFF/g, "")
@@ -41,47 +43,97 @@ function toAscii(text) {
4143
.replace(/[]/g, "-");
4244
}
4345

44-
function printUsage() {
45-
console.error(
46-
"Usage: node scripts/print-method-snippet.js <resource-id> [locale] [--ascii]",
46+
export function getActiveWindowsCodePage() {
47+
const override = process.env.APIOPS_WINDOWS_CODEPAGE;
48+
if (override) {
49+
return Number.parseInt(override, 10);
50+
}
51+
52+
const result = spawnSync(
53+
process.env.ComSpec || "cmd.exe",
54+
["/d", "/s", "/c", "chcp"],
55+
{
56+
encoding: "utf8",
57+
windowsHide: true,
58+
},
4759
);
60+
61+
const output = `${result.stdout || ""}\n${result.stderr || ""}`;
62+
const match = output.match(/(\d{3,5})/);
63+
return match ? Number.parseInt(match[1], 10) : null;
4864
}
4965

50-
const args = process.argv.slice(2);
51-
const ascii = args.includes("--ascii");
52-
const positional = args.filter((arg) => arg !== "--ascii");
53-
const resourceId = positional[0];
54-
const locale = positional[1] || "en";
66+
export function shouldUseAsciiFallback(forceAscii, forceUnicode) {
67+
if (forceAscii) {
68+
return true;
69+
}
5570

56-
if (!resourceId) {
57-
printUsage();
58-
process.exit(1);
59-
}
71+
if (forceUnicode) {
72+
return false;
73+
}
6074

61-
const resources = readJson(resolveMethodFile("resources.json")).resources;
62-
const resource = resources.find((entry) => entry.id === resourceId);
75+
const isWindows = process.platform === "win32" || process.env.APIOPS_FORCE_WINDOWS === "1";
76+
if (!isWindows) {
77+
return false;
78+
}
6379

64-
if (!resource) {
65-
console.error(`Unknown resource id: ${resourceId}`);
66-
process.exit(1);
80+
const codePage = getActiveWindowsCodePage();
81+
return codePage !== null && codePage !== 65001;
6782
}
6883

69-
if (!resource.snippet) {
70-
console.error(`Resource ${resourceId} does not define a snippet`);
71-
process.exit(1);
84+
function printUsage() {
85+
console.error(
86+
"Usage: node scripts/print-method-snippet.js <resource-id> [locale] [--ascii] [--unicode]",
87+
);
7288
}
7389

74-
const snippetPath = resolveSnippetPath(resource.snippet, locale);
90+
export function renderSnippet(resourceId, locale = "en", options = {}) {
91+
const resources = readJson(resolveMethodFile("resources.json")).resources;
92+
const resource = resources.find((entry) => entry.id === resourceId);
93+
94+
if (!resource) {
95+
throw new Error(`Unknown resource id: ${resourceId}`);
96+
}
97+
98+
if (!resource.snippet) {
99+
throw new Error(`Resource ${resourceId} does not define a snippet`);
100+
}
101+
102+
const snippetPath = resolveSnippetPath(resource.snippet, locale);
103+
if (!fs.existsSync(snippetPath)) {
104+
throw new Error(`Snippet not found: ${snippetPath}`);
105+
}
75106

76-
if (!fs.existsSync(snippetPath)) {
77-
console.error(`Snippet not found: ${snippetPath}`);
78-
process.exit(1);
107+
let content = fs.readFileSync(snippetPath, "utf8");
108+
if (shouldUseAsciiFallback(options.forceAscii, options.forceUnicode)) {
109+
content = toAscii(content);
110+
}
111+
112+
return content;
79113
}
80114

81-
let content = fs.readFileSync(snippetPath, "utf8");
115+
function main() {
116+
const args = process.argv.slice(2);
117+
const forceAscii = args.includes("--ascii");
118+
const forceUnicode = args.includes("--unicode");
119+
const positional = args.filter((arg) => arg !== "--ascii" && arg !== "--unicode");
120+
const resourceId = positional[0];
121+
const locale = positional[1] || "en";
122+
123+
if (!resourceId) {
124+
printUsage();
125+
process.exit(1);
126+
}
82127

83-
if (ascii) {
84-
content = toAscii(content);
128+
try {
129+
const content = renderSnippet(resourceId, locale, { forceAscii, forceUnicode });
130+
process.stdout.write(content, "utf8");
131+
} catch (error) {
132+
console.error(error instanceof Error ? error.message : error);
133+
process.exit(1);
134+
}
85135
}
86136

87-
process.stdout.write(content);
137+
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
138+
main();
139+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { mkdtempSync, mkdirSync, cpSync, rmSync } from "node:fs";
2+
import { tmpdir } from "node:os";
3+
import { fileURLToPath } from "node:url";
4+
import { dirname, join, resolve } from "node:path";
5+
import { renderSnippet } from "../packages/create-apiops/template/scripts/print-method-snippet.js";
6+
7+
const __filename = fileURLToPath(import.meta.url);
8+
const __dirname = dirname(__filename);
9+
const repoRoot = resolve(__dirname, "..");
10+
const tempRoot = mkdtempSync(join(tmpdir(), "print-method-snippet-"));
11+
const fixturePkgRoot = join(tempRoot, "node_modules", "apiops-cycles-method-data");
12+
const previousCwd = process.cwd();
13+
14+
function assert(condition, message) {
15+
if (!condition) {
16+
throw new Error(message);
17+
}
18+
}
19+
20+
try {
21+
mkdirSync(join(fixturePkgRoot, "src", "data", "method"), { recursive: true });
22+
mkdirSync(join(fixturePkgRoot, "src", "snippets"), { recursive: true });
23+
cpSync(resolve(repoRoot, "src", "data", "method", "resources.json"), join(fixturePkgRoot, "src", "data", "method", "resources.json"));
24+
cpSync(resolve(repoRoot, "src", "snippets", "api-audit-checklist.md"), join(fixturePkgRoot, "src", "snippets", "api-audit-checklist.md"));
25+
26+
process.chdir(tempRoot);
27+
28+
const unicodeOutput = renderSnippet("api-audit-checklist", "en", { forceUnicode: true });
29+
assert(unicodeOutput.includes("### ✅ Concept is Ready When..."), "Expected forced Unicode output to keep checklist symbols.");
30+
assert(unicodeOutput.includes("### 🧪 API Design Prototype is Ready When..."), "Expected forced Unicode output to keep prototype heading.");
31+
32+
const explicitAsciiOutput = renderSnippet("api-audit-checklist", "en", { forceAscii: true });
33+
assert(explicitAsciiOutput.includes("### [OK] Concept is Ready When..."), "Expected --ascii output to replace heading symbols.");
34+
assert(explicitAsciiOutput.includes("| API is based on clear business needs | API9:2019 | [X]"), "Expected --ascii output to replace checklist cross marks.");
35+
36+
process.env.APIOPS_FORCE_WINDOWS = "1";
37+
process.env.APIOPS_WINDOWS_CODEPAGE = "437";
38+
const simulatedWindowsOutput = renderSnippet("api-audit-checklist", "en");
39+
assert(simulatedWindowsOutput.includes("### [OK] Concept is Ready When..."), "Expected Windows non-UTF-8 fallback to use ASCII automatically.");
40+
assert(simulatedWindowsOutput.includes("### [Prod] API Is Maintainable in Production When..."), "Expected production heading to remain readable in fallback mode.");
41+
42+
delete process.env.APIOPS_FORCE_WINDOWS;
43+
delete process.env.APIOPS_WINDOWS_CODEPAGE;
44+
console.log("print-method-snippet regression test passed.");
45+
} finally {
46+
delete process.env.APIOPS_FORCE_WINDOWS;
47+
delete process.env.APIOPS_WINDOWS_CODEPAGE;
48+
process.chdir(previousCwd);
49+
rmSync(tempRoot, { recursive: true, force: true });
50+
}

0 commit comments

Comments
 (0)