Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
311 changes: 69 additions & 242 deletions package-lock.json

Large diffs are not rendered by default.

6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "create-next2d-app",
"version": "2.5.1",
"version": "2.6.0",
"description": "Create Next2D apps with no build configuration.",
"author": "Toshiyuki Ienaga<ienaga@next2d.app>",
"license": "MIT",
Expand All @@ -14,7 +14,7 @@
"create-next2d-app"
],
"scripts": {
"lint": "eslint src/index.ts",
"lint": "eslint src",
"release": "tsc",
"test": "vitest run --coverage"
},
Expand All @@ -37,7 +37,7 @@
"@vitest/coverage-v8": "^5.0.5",
"eslint": "^10.14.0",
"eslint-plugin-unused-imports": "^4.4.1",
"globals": "^17.13.0",
"globals": "^17.14.0",
"typescript": "^6.0.3",
"vitest": "^5.0.5"
}
Expand Down
60 changes: 13 additions & 47 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import os from "os";
import { gte as semverGte } from "./lib/semver.js";
import spawn from "./lib/spawn.js";
import { initializeElectronConfig } from "./lib/electron-config.js";
import { initializeXboxConfig } from "./lib/xbox-config.js";
import { createIgnoreList, createScripts } from "./lib/project-files.js";

const recommendedVersion: number = 22;
const version: string = process.versions.node;
Expand Down Expand Up @@ -173,6 +175,9 @@ interface Packages {
},
devDependencies?: {
[key: string]: string
},
scripts?: {
[key: string]: string
}
}

Expand Down Expand Up @@ -245,6 +250,10 @@ const install = (

const templatePackage: Packages | void = templateJson.package;
if (templatePackage) {
if (templatePackage.scripts) {
packageJson.scripts = createScripts(templatePackage.scripts);
}

const templateDependencies = templatePackage.dependencies;
if (templateDependencies) {
const keys: string[] = Object.keys(templateDependencies);
Expand Down Expand Up @@ -283,6 +292,7 @@ const install = (
if (fs.existsSync(templateDir)) {
fs.copySync(templateDir, root);
initializeElectronConfig(root, app_name);
initializeXboxConfig(root, app_name);
} else {
console.error(
`Could not locate supplied template: ${pc.green(templateDir)}`
Expand Down Expand Up @@ -425,26 +435,7 @@ const createApp = (
"version": "0.0.1",
"private": true,
"type": "module",
"scripts": {
"start": "vite --host",
"preview:ios": "npx -y @next2d/builder --platform ios --preview",
"preview:android": "npx -y @next2d/builder --platform android --preview",
"preview:macos": "npx -y @next2d/builder --platform macos --preview",
"preview:windows": "npx -y @next2d/builder --platform windows --preview",
"preview:linux": "npx -y @next2d/builder --platform linux --preview",
"open:ios": "npx -y @next2d/builder --platform ios --open",
"open:android": "npx -y @next2d/builder --platform android --open",
"build:steam:windows": "npx -y @next2d/builder --platform steam:windows --env prd",
"build:steam:macos": "npx -y @next2d/builder --platform steam:macos --env prd",
"build:steam:linux": "npx -y @next2d/builder --platform steam:linux --env prd",
"build:steam:manifest": "npx -y @next2d/builder --env prd --steam-manifest",
"build:web": "npx -y @next2d/builder --platform web --env prd",
"build:ios": "npx -y @next2d/builder --platform ios --env prd --build",
"build:android": "npx -y @next2d/builder --platform android --env prd --build",
"build:xbox": "npx -y @next2d/builder --platform xbox --env prd",
"test": "npx vitest",
"generate": "npx -y @next2d/view-generator"
}
"scripts": createScripts()
}, null, 2) + os.EOL
);

Expand All @@ -465,36 +456,11 @@ const createApp = (
}
}

const ignoreList: string[] = [
"# Logs",
"logs",
"*.log",
"npm-debug.log*",
"yarn-debug.log*",
"yarn-error.log*",
"pnpm-debug.log*",
"lerna-debug.log*",
"node_modules",
"dist",
"dist-ssr",
"*.local",
"# Editor directories and files",
".vscode/*",
"!.vscode/extensions.json",
".idea",
".DS_Store",
"*.suo",
"*.ntvs*",
"*.njsproj",
"*.sln",
"*.sw?",
"src/Packages.ts",
"src/config/Config.ts"
];
const ignoreList: string[] = createIgnoreList();

fs.writeFileSync(
path.join(root, ".gitignore"),
ignoreList.join(os.EOL)
ignoreList.join(os.EOL) + os.EOL
);

install(root, appName, template, ["@next2d/framework"], [
Expand Down
2 changes: 1 addition & 1 deletion src/lib/colors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ const isColorSupported: boolean =
!(!!env.NO_COLOR || argv.includes("--no-color")) &&
(!!env.FORCE_COLOR || argv.includes("--color") ||
process.platform === "win32" ||
((process.stdout || {}).isTTY && env.TERM !== "dumb") ||
(process.stdout || {}).isTTY && env.TERM !== "dumb" ||
!!env.CI);

const replaceClose = (
Expand Down
87 changes: 87 additions & 0 deletions src/lib/project-files.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
const BUILDER: string = "npx -y @next2d/builder";

/**
* @description 生成するプロジェクトの npm scripts。
* テンプレートの template.json に `package.scripts` があれば上書き・追加する。
* npm scripts of the generated project. `package.scripts` in the
* template's template.json overrides or extends these.
*
* @param {Record<string, string>} [templateScripts]
* @return {Record<string, string>}
* @method
* @public
*/
export const createScripts = (
templateScripts: Record<string, string> = {}
): Record<string, string> => {
return {
"start": "vite --host",
"preview:web": `${BUILDER} --platform web --env prd --preview`,
"preview:ios": `${BUILDER} --platform ios --env prd --preview`,
"preview:android": `${BUILDER} --platform android --env prd --preview`,
"preview:macos": `${BUILDER} --platform macos --env prd --preview`,
"preview:windows": `${BUILDER} --platform windows --env prd --preview`,
"preview:linux": `${BUILDER} --platform linux --env prd --preview`,
"open:ios": `${BUILDER} --platform ios --env prd --open`,
"open:android": `${BUILDER} --platform android --env prd --open`,
"open:xbox": `${BUILDER} --platform xbox --env prd --open`,
"build:web": `${BUILDER} --platform web --env prd`,
"build:ios": `${BUILDER} --platform ios --env prd --build`,
"build:android": `${BUILDER} --platform android --env prd --build`,
"build:xbox": `${BUILDER} --platform xbox --env prd`,
"build:steam:windows": `${BUILDER} --platform steam:windows --env prd`,
"build:steam:macos": `${BUILDER} --platform steam:macos --env prd`,
"build:steam:linux": `${BUILDER} --platform steam:linux --env prd`,
// builder は --steam-manifest に steam:* の --platform を必須とする (OS はどれでもよい)
"build:steam:manifest": `${BUILDER} --platform steam:macos --env prd --steam-manifest`,
"upload:steam": `${BUILDER} --steam-upload --env prd`,
"upload:steam:dry-run": `${BUILDER} --steam-upload --env prd --dry-run`,
"test": "npx vitest",
"generate": "npx -y @next2d/view-generator",
...templateScripts
};
};

/**
* @description 生成するプロジェクトの .gitignore の行。
* npm はテンプレート内の .gitignore を公開しないため、ここで生成する。
* Lines of the generated .gitignore. npm does not publish a
* template's .gitignore, so it is generated here.
*
* @return {string[]}
* @method
* @public
*/
export const createIgnoreList = (): string[] => {
return [
"# Logs",
"logs",
"*.log",
"npm-debug.log*",
"yarn-debug.log*",
"yarn-error.log*",
"pnpm-debug.log*",
"lerna-debug.log*",
"node_modules",
"dist",
"dist-ssr",
"*.local",
"# Editor directories and files",
".vscode/*",
"!.vscode/extensions.json",
".idea",
".DS_Store",
"*.suo",
"*.ntvs*",
"*.njsproj",
"*.sln",
"*.sw?",
"# Generated by @next2d/vite-plugin-next2d-auto-loader",
"src/Packages.ts",
"src/Packages.js",
"src/config/Config.ts",
"src/config/Config.js",
"# Xbox: C++/CMake host generated by @next2d/builder (MicrosoftGame.config is kept at the root)",
"/xbox"
];
};
4 changes: 2 additions & 2 deletions src/lib/spawn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ const spawnFn = (
): ChildProcess => {
const opts: SpawnOptions = isWindows
? { ...options, "shell": true }
: (options ?? {});
: options ?? {};
return nodeSpawn(command, args ?? [], opts);
};

Expand All @@ -35,7 +35,7 @@ const spawnSyncFn = (
) => {
const opts: SpawnSyncOptions = isWindows
? { ...options, "shell": true }
: (options ?? {});
: options ?? {};
return nodeSpawnSync(command, args ?? [], opts);
};

Expand Down
47 changes: 47 additions & 0 deletions src/lib/xbox-config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import fs from "node:fs";
import path from "node:path";

/**
* @description XML 属性値として安全な文字列に変換する
* Escape a string for use as an XML attribute value
*
* @param {string} value
* @return {string}
* @method
* @private
*/
const escapeXmlAttribute = (value: string): string => {
return value
.replace(/&/g, "&amp;")
.replace(/"/g, "&quot;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;");
};

/**
* @description テンプレートコピー後、MicrosoftGame.config の表示名をプロジェクト名にする。
* ID@Xbox 発行値 (TitleId / StoreId 等) はユーザーが置き換えるため変更しない。
* Set the display name in MicrosoftGame.config to the project name after
* copying a template. Values issued by ID@Xbox are left for the user.
*
* @param {string} root
* @param {string} appName
* @return {void}
* @method
* @public
*/
export const initializeXboxConfig = (root: string, appName: string): void => {
const configPath = path.join(root, "MicrosoftGame.config");
// Custom templates without Xbox support need no GDK configuration.
if (!fs.existsSync(configPath)) {
return;
}
const source = fs.readFileSync(configPath, "utf8");
const updated = source.replace(
/(<ShellVisuals\b[^>]*?\bDefaultDisplayName=")[^"]*(")/,
(_match: string, before: string, after: string): string => `${before}${escapeXmlAttribute(appName)}${after}`
);
if (updated !== source) {
fs.writeFileSync(configPath, updated);
}
};
33 changes: 33 additions & 0 deletions test/project-files.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { test, expect } from "vitest";
import { createIgnoreList, createScripts } from "../src/lib/project-files";

test("every builder script selects a target and a default environment", () => {
const builderScripts = Object.entries(createScripts())
.filter(([, command]) => command.startsWith("npx -y @next2d/builder "));
expect(builderScripts.length).toBeGreaterThan(0);
for (const [name, command] of builderScripts) {
const args = command.split(" ");
expect(args.includes("--platform") || args.includes("--steam-upload"), name).toBe(true);
expect(args[args.indexOf("--env") + 1], name).toBe("prd");
expect(command, name).not.toMatch(/ {2}/);
}
});

test("steam manifest script passes a steam platform", () => {
expect(createScripts()["build:steam:manifest"]).toContain("--platform steam:");
});

test("template scripts override and extend the defaults", () => {
const scripts = createScripts({ "lint": "eslint src", "test": "vitest run" });
expect(scripts.lint).toBe("eslint src");
expect(scripts.test).toBe("vitest run");
expect(scripts.start).toBe("vite --host");
});

test("gitignore covers generated files of JavaScript and TypeScript templates and the Xbox host", () => {
const list = createIgnoreList();
for (const entry of ["node_modules", "dist", "src/Packages.ts", "src/Packages.js", "src/config/Config.ts", "src/config/Config.js", "/xbox"]) {
expect(list).toContain(entry);
}
expect(list).not.toContain("MicrosoftGame.config");
});
56 changes: 56 additions & 0 deletions test/xbox-config.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { test, expect, afterEach } from "vitest";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { initializeXboxConfig } from "../src/lib/xbox-config";

const roots: string[] = [];
const fixture = (): string => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), "next2d-xbox-config-"));
roots.push(root);
return root;
};
afterEach(() => {
for (const root of roots.splice(0)) {
fs.rmSync(root, { "recursive": true, "force": true });
}
});

const CONFIG = `<?xml version="1.0" encoding="utf-8"?>
<Game configVersion="1">
<Identity Name="Next2DGame" Publisher="CN=Next2D" Version="1.0.0.0" />
<ShellVisuals DefaultDisplayName="Example-Product-Name"
PublisherDisplayName="Next2D" />
<TitleId>00000000</TitleId>
</Game>
`;

test("sets the display name and keeps ID@Xbox values", () => {
const root = fixture();
const file = path.join(root, "MicrosoftGame.config");
fs.writeFileSync(file, CONFIG);
initializeXboxConfig(root, "my-game");
expect(fs.readFileSync(file, "utf8")).toBe(CONFIG.replace("Example-Product-Name", "my-game"));
});

test("escapes XML special characters in the display name", () => {
const root = fixture();
const file = path.join(root, "MicrosoftGame.config");
fs.writeFileSync(file, CONFIG);
initializeXboxConfig(root, "a&\"<b>");
expect(fs.readFileSync(file, "utf8")).toContain("DefaultDisplayName=\"a&amp;&quot;&lt;b&gt;\"");
});

test("leaves configs without ShellVisuals unchanged", () => {
const root = fixture();
const file = path.join(root, "MicrosoftGame.config");
fs.writeFileSync(file, "<Game />");
initializeXboxConfig(root, "my-game");
expect(fs.readFileSync(file, "utf8")).toBe("<Game />");
});

test("custom templates without Xbox configuration remain unchanged", () => {
const root = fixture();
initializeXboxConfig(root, "web-game");
expect(fs.readdirSync(root)).toEqual([]);
});
Loading