184 lines
7.7 KiB
JavaScript
184 lines
7.7 KiB
JavaScript
// Sanity checks for the VS Code extension assets. Run with `node test/validate.mjs`.
|
|
// Verifies the JSON contribution files parse and that the bundled compiler both
|
|
// accepts valid .wrn and reports an offset for invalid .wrn (what diagnostics need).
|
|
import { readFileSync } from "node:fs";
|
|
import { fileURLToPath } from "node:url";
|
|
import { dirname, join } from "node:path";
|
|
import { createRequire } from "node:module";
|
|
|
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
const root = join(here, "..");
|
|
const require = createRequire(import.meta.url);
|
|
|
|
let failures = 0;
|
|
const ok = (name) => console.log(` ok ${name}`);
|
|
const bad = (name, detail) => {
|
|
failures++;
|
|
console.error(` FAIL ${name}${detail ? " — " + detail : ""}`);
|
|
};
|
|
|
|
// 1. All JSON contribution files parse.
|
|
for (const rel of [
|
|
"package.json",
|
|
"language-configuration.json",
|
|
"syntaxes/wrn.tmLanguage.json",
|
|
"syntaxes/wrn.injection.tmLanguage.json",
|
|
"snippets/wrn.json",
|
|
]) {
|
|
try {
|
|
JSON.parse(readFileSync(join(root, rel), "utf8"));
|
|
ok(`json parses: ${rel}`);
|
|
} catch (e) {
|
|
bad(`json parses: ${rel}`, e.message);
|
|
}
|
|
}
|
|
|
|
// 2. Grammar connects up the right scope + embedded languages.
|
|
const grammar = JSON.parse(readFileSync(join(root, "syntaxes/wrn.tmLanguage.json"), "utf8"));
|
|
grammar.scopeName === "source.wrn" ? ok("grammar scopeName") : bad("grammar scopeName");
|
|
|
|
const localIncludes = [];
|
|
const visitGrammarNode = (value) => {
|
|
if (Array.isArray(value)) {
|
|
value.forEach(visitGrammarNode);
|
|
return;
|
|
}
|
|
if (!value || typeof value !== "object") return;
|
|
if (typeof value.include === "string" && value.include.startsWith("#")) {
|
|
localIncludes.push(value.include.slice(1));
|
|
}
|
|
Object.values(value).forEach(visitGrammarNode);
|
|
};
|
|
visitGrammarNode(grammar.patterns);
|
|
visitGrammarNode(grammar.repository);
|
|
const missingIncludes = [...new Set(localIncludes)].filter((name) => !grammar.repository?.[name]);
|
|
missingIncludes.length === 0
|
|
? ok("all local grammar includes resolve")
|
|
: bad("all local grammar includes resolve", missingIncludes.join(", "));
|
|
grammar.repository?.["conditional-class-single"]
|
|
? ok("single-quoted class directives are registered")
|
|
: bad("single-quoted class directives are registered");
|
|
grammar.repository?.["style-block"]?.applyEndPatternLast === true
|
|
? ok("style blocks keep nested CSS braces embedded")
|
|
: bad("style blocks keep nested CSS braces embedded");
|
|
grammar.repository?.["view-block"]?.applyEndPatternLast === true
|
|
? ok("view blocks keep template directive braces embedded")
|
|
: bad("view blocks keep template directive braces embedded");
|
|
grammar.repository?.interpolation?.patterns?.some(
|
|
(pattern) => pattern.name === "meta.block.directive.wrn",
|
|
)
|
|
? ok("template block directives have dedicated grammar scopes")
|
|
: bad("template block directives have dedicated grammar scopes");
|
|
|
|
// 3. Marketplace metadata and the gallery icon meet vsce requirements.
|
|
try {
|
|
const manifest = JSON.parse(readFileSync(join(root, "package.json"), "utf8"));
|
|
manifest.publisher === "wrnexus" ? ok("publisher id") : bad("publisher id");
|
|
const language = manifest.contributes?.languages?.find((entry) => entry.id === "wrn");
|
|
language?.extensions?.includes(".wrn")
|
|
? ok(".wrn extension maps to the wrn language")
|
|
: bad(".wrn extension maps to the wrn language");
|
|
manifest.contributes?.configurationDefaults?.["files.associations"]?.["*.wrn"] === "wrn"
|
|
? ok("default file association uses the wrn language id")
|
|
: bad("default file association uses the wrn language id");
|
|
manifest.activationEvents?.includes("onLanguage:wrn")
|
|
? ok("extension activates for the wrn language")
|
|
: bad("extension activates for the wrn language");
|
|
manifest.activationEvents?.length === 1 && manifest.activationEvents[0] === "onLanguage:wrn"
|
|
? ok("extension activates only when a WRN document is used")
|
|
: bad("extension avoids workspace-wide startup activation");
|
|
const extensionSource = readFileSync(join(root, "src/extension.js"), "utf8");
|
|
!extensionSource.includes('createFileSystemWatcher("**/*.wrn")')
|
|
? ok("language client avoids an unrestricted workspace watcher")
|
|
: bad("language client avoids an unrestricted workspace watcher");
|
|
manifest.contributes?.configuration?.properties?.["wrnexus.languageServer.enable"]?.default ===
|
|
true
|
|
? ok("shared language server is enabled by default")
|
|
: bad("shared language server is enabled by default");
|
|
readFileSync(join(root, "src/language-server.cjs"), "utf8").includes("WRNexus Language Server")
|
|
? ok("bundled language server exists")
|
|
: bad("bundled language server exists");
|
|
language?.firstLine
|
|
? ok("first-line language detection is configured")
|
|
: bad("first-line language detection is configured");
|
|
manifest.icon?.endsWith(".png") ? ok("Marketplace icon is PNG") : bad("Marketplace icon is PNG");
|
|
const icon = readFileSync(join(root, manifest.icon));
|
|
const isPng = icon.subarray(1, 4).toString("ascii") === "PNG";
|
|
const width = isPng ? icon.readUInt32BE(16) : 0;
|
|
const height = isPng ? icon.readUInt32BE(20) : 0;
|
|
isPng && width >= 128 && height >= 128
|
|
? ok(`Marketplace icon dimensions (${width}x${height})`)
|
|
: bad("Marketplace icon is at least 128x128 PNG");
|
|
for (const rel of ["README.md", "CHANGELOG.md", "SUPPORT.md", "LICENSE"]) {
|
|
readFileSync(join(root, rel), "utf8");
|
|
ok(`Marketplace document exists: ${rel}`);
|
|
}
|
|
} catch (e) {
|
|
bad("Marketplace metadata", e.message);
|
|
}
|
|
|
|
// 4. Bundled compiler: valid source compiles, invalid source throws with an offset.
|
|
try {
|
|
const compiler = require(join(root, "src/compiler.cjs"));
|
|
const good = `page Home {\n view { <h1>Hi</h1> }\n}`;
|
|
compiler.compileWrnFile(good);
|
|
ok("compiler accepts valid .wrn");
|
|
|
|
const structuredProps = `page FooterExample {
|
|
state footerItems = [
|
|
{
|
|
"label": "Accessibility",
|
|
"href": "/accessibility",
|
|
"value": "accessibility"
|
|
}
|
|
]
|
|
view {
|
|
<Footer
|
|
items={footerItems}
|
|
options={{"dense":true}}
|
|
links={[{"label":"Privacy","href":"/privacy"}]}
|
|
/>
|
|
}
|
|
}`;
|
|
compiler.compileWrnFile(structuredProps);
|
|
ok("compiler accepts native structured state and unquoted prop expressions");
|
|
|
|
const badSrc = `page Home {\n view { <h1>Hi</h2> }\n}`;
|
|
let threw = false;
|
|
try {
|
|
compiler.compileWrnFile(badSrc);
|
|
} catch (e) {
|
|
threw = true;
|
|
/offset\s+\d+|end of input/.test(e.message)
|
|
? ok("compiler reports a locatable error")
|
|
: bad("compiler reports a locatable error", e.message);
|
|
}
|
|
if (!threw) bad("compiler rejects invalid .wrn");
|
|
} catch (e) {
|
|
bad("compiler bundle loads", e.message);
|
|
}
|
|
|
|
// 5. Formatter normalizes nested WRN blocks and is idempotent.
|
|
try {
|
|
const { formatWrn } = require(join(root, "src/formatter.js"));
|
|
const source = `page Home {\nview {\n<main>\n<h1>Hello</h1>\n</main>\n}\n}\n`;
|
|
const expected = `page Home {\n view {\n <main>\n <h1>Hello</h1>\n </main>\n }\n}\n`;
|
|
const formatted = formatWrn(source, { insertSpaces: true, tabSize: 2 });
|
|
formatted === expected ? ok("formatter indents WRN blocks") : bad("formatter indents WRN blocks");
|
|
formatWrn(formatted, { insertSpaces: true, tabSize: 2 }) === formatted
|
|
? ok("formatter is idempotent")
|
|
: bad("formatter is idempotent");
|
|
const imported = formatWrn(
|
|
`import {\nappUrl,\nother\n} from "@wrnexus/helpers";\n\nlayout Public {\nview { <p>Hi</p> }\n}\n`,
|
|
{ insertSpaces: true, tabSize: 2 },
|
|
);
|
|
imported.includes(`\nlayout Public {`) && formatWrn(imported, { tabSize: 2 }) === imported
|
|
? ok("formatter preserves top-level imports")
|
|
: bad("formatter preserves top-level imports");
|
|
} catch (e) {
|
|
bad("formatter loads", e.message);
|
|
}
|
|
|
|
console.log(failures === 0 ? "\nAll extension checks passed." : `\n${failures} check(s) failed.`);
|
|
process.exit(failures === 0 ? 0 : 1);
|