create-m5kdev 0.25.3 → 0.26.1
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/dist/src/create.cjs +18 -1
- package/dist/src/create.cjs.map +1 -1
- package/dist/src/fs.cjs +65 -5
- package/dist/src/fs.cjs.map +1 -1
- package/dist/src/fs.d.cts +13 -2
- package/dist/src/index.cjs +0 -0
- package/dist/src/paths.cjs +2 -2
- package/dist/src/prompts.cjs +22 -0
- package/dist/src/prompts.cjs.map +1 -1
- package/dist/src/runCli.cjs +13 -2
- package/dist/src/runCli.cjs.map +1 -1
- package/dist/src/types.d.cts +9 -1
- package/package.json +8 -6
- package/templates/minimal-app/.cursor/rules/frontend-data-guide.mdc +46 -3
- package/templates/minimal-app/.cursor/rules/module-db-guide.mdc +15 -1
- package/templates/minimal-app/.cursor/rules/module-dto-guide.mdc +60 -0
- package/templates/minimal-app/.cursor/rules/module-repository-guide.mdc +98 -193
- package/templates/minimal-app/.cursor/rules/module-schema-guide.mdc +53 -173
- package/templates/minimal-app/.cursor/rules/module-service-guide.mdc +133 -240
- package/templates/minimal-app/.cursor/rules/module-trpc-guide.mdc +71 -113
- package/templates/minimal-app/README.md.tpl +3 -3
- package/templates/minimal-app/apps/.gitignore.tpl +13 -0
- package/templates/minimal-app/apps/e2e/package.json.tpl +23 -0
- package/templates/minimal-app/apps/e2e/playwright.config.ts.tpl +39 -0
- package/templates/minimal-app/apps/e2e/playwright.expo.config.ts.tpl +38 -0
- package/templates/minimal-app/apps/e2e/servers.ts.tpl +100 -0
- package/templates/minimal-app/apps/e2e/tests/email.spec.ts.tpl +47 -0
- package/templates/minimal-app/apps/e2e/tests/expo-helpers.ts.tpl +73 -0
- package/templates/minimal-app/apps/e2e/tests/expo-standard.spec.ts.tpl +517 -0
- package/templates/minimal-app/apps/e2e/tests/expo-waitlist.spec.ts.tpl +94 -0
- package/templates/minimal-app/apps/e2e/tests/helpers.ts.tpl +289 -0
- package/templates/minimal-app/apps/e2e/tests/standard.spec.ts.tpl +516 -0
- package/templates/minimal-app/apps/e2e/tests/waitlist.spec.ts.tpl +107 -0
- package/templates/minimal-app/apps/e2e/tsconfig.json.tpl +10 -0
- package/templates/minimal-app/apps/email/package.json.tpl +34 -34
- package/templates/minimal-app/apps/email/src/emails/accountDeletionEmail.tsx.tpl +35 -34
- package/templates/minimal-app/apps/email/src/emails/organizationInviteEmail.tsx.tpl +34 -33
- package/templates/minimal-app/apps/email/src/emails/passwordResetEmail.tsx.tpl +35 -34
- package/templates/minimal-app/apps/email/src/emails/verificationEmail.tsx.tpl +37 -36
- package/templates/minimal-app/apps/email/src/emails/waitlistEmails.tsx.tpl +87 -0
- package/templates/minimal-app/apps/email/src/index.ts.tpl +72 -32
- package/templates/minimal-app/apps/email/src/resources.ts.tpl +65 -36
- package/templates/minimal-app/apps/email/tsconfig.json.tpl +16 -11
- package/templates/minimal-app/apps/expo/.gitignore.tpl +44 -0
- package/templates/minimal-app/apps/expo/.prettierignore +13 -0
- package/templates/minimal-app/apps/expo/.prettierrc.json.tpl +9 -0
- package/templates/minimal-app/apps/expo/CHANGELOG.md.tpl +156 -0
- package/templates/minimal-app/apps/expo/README.md.tpl +44 -0
- package/templates/minimal-app/apps/expo/app.json.tpl +36 -0
- package/templates/minimal-app/apps/expo/assets/images/adaptive-icon.png +0 -0
- package/templates/minimal-app/apps/expo/assets/images/favicon.png +0 -0
- package/templates/minimal-app/apps/expo/assets/images/icon.png +0 -0
- package/templates/minimal-app/apps/expo/assets/images/splash-icon.png +0 -0
- package/templates/minimal-app/apps/expo/eslint.config.js.tpl +8 -0
- package/templates/minimal-app/apps/expo/expo-env.d.ts.tpl +3 -0
- package/templates/minimal-app/apps/expo/metro.config.js.tpl +16 -0
- package/templates/minimal-app/apps/expo/package.json.tpl +59 -0
- package/templates/minimal-app/apps/expo/src/app/_layout.tsx.tpl +31 -0
- package/templates/minimal-app/apps/expo/src/app/claim-account.tsx.tpl +151 -0
- package/templates/minimal-app/apps/expo/src/app/forgot-password.tsx.tpl +53 -0
- package/templates/minimal-app/apps/expo/src/app/index.tsx.tpl +8 -0
- package/templates/minimal-app/apps/expo/src/app/login.tsx.tpl +71 -0
- package/templates/minimal-app/apps/expo/src/app/logout.tsx.tpl +19 -0
- package/templates/minimal-app/apps/expo/src/app/organization/accept-invitation.tsx.tpl +85 -0
- package/templates/minimal-app/apps/expo/src/app/posts.tsx.tpl +40 -0
- package/templates/minimal-app/apps/expo/src/app/reset-password.tsx.tpl +75 -0
- package/templates/minimal-app/apps/expo/src/app/signup.tsx.tpl +193 -0
- package/templates/minimal-app/apps/expo/src/components/AuthScaffold.tsx.tpl +34 -0
- package/templates/minimal-app/apps/expo/src/components/FormTextField.tsx.tpl +37 -0
- package/templates/minimal-app/apps/expo/src/components/LoadingScreen.tsx.tpl +10 -0
- package/templates/minimal-app/apps/expo/src/components/StatusText.tsx.tpl +22 -0
- package/templates/minimal-app/apps/expo/src/config.ts.tpl +9 -0
- package/templates/minimal-app/apps/expo/src/global.css.tpl +11 -0
- package/templates/minimal-app/apps/expo/src/lib/trpc.ts.tpl +20 -0
- package/templates/minimal-app/apps/expo/tsconfig.json.tpl +11 -0
- package/templates/minimal-app/apps/expo/uniwind-env.d.ts.tpl +1 -0
- package/templates/minimal-app/apps/expo/uniwind-types.d.ts.tpl +10 -0
- package/templates/minimal-app/apps/server/AGENTS.md.tpl +39 -39
- package/templates/minimal-app/apps/server/drizzle/0000_wealthy_absorbing_man.sql +268 -0
- package/templates/minimal-app/apps/server/drizzle/db.ts.tpl +26 -0
- package/templates/minimal-app/apps/server/drizzle/meta/0000_snapshot.json.tpl +1890 -0
- package/templates/minimal-app/apps/server/drizzle/meta/_journal.json.tpl +13 -0
- package/templates/minimal-app/apps/server/drizzle/reset.ts.tpl +33 -0
- package/templates/minimal-app/apps/server/drizzle/seed.e2e.ts.tpl +244 -0
- package/templates/minimal-app/apps/server/drizzle/seed.ts.tpl +181 -180
- package/templates/minimal-app/apps/server/drizzle/sync.ts.tpl +14 -21
- package/templates/minimal-app/apps/server/drizzle.config.ts.tpl +33 -35
- package/templates/minimal-app/apps/server/package.json.tpl +56 -52
- package/templates/minimal-app/apps/server/src/app.ts.tpl +142 -126
- package/templates/minimal-app/apps/server/src/index.ts.tpl +58 -58
- package/templates/minimal-app/apps/server/src/lib/auth.ts.tpl +12 -12
- package/templates/minimal-app/apps/server/src/modules/posts/posts.db.ts.tpl +25 -25
- package/templates/minimal-app/apps/server/src/modules/posts/posts.dto.ts.tpl +23 -0
- package/templates/minimal-app/apps/server/src/modules/posts/posts.grants.ts.tpl +30 -40
- package/templates/minimal-app/apps/server/src/modules/posts/posts.module.ts.tpl +54 -54
- package/templates/minimal-app/apps/server/src/modules/posts/posts.repository.ts.tpl +67 -112
- package/templates/minimal-app/apps/server/src/modules/posts/posts.service.ts.tpl +104 -170
- package/templates/minimal-app/apps/server/src/modules/posts/posts.trpc.ts.tpl +37 -48
- package/templates/minimal-app/apps/server/src/modules/posts/posts.utils.ts.tpl +19 -0
- package/templates/minimal-app/apps/server/src/modules/test-harness/test-harness.module.ts.tpl +279 -0
- package/templates/minimal-app/apps/server/src/schema.ts.tpl +20 -0
- package/templates/minimal-app/apps/server/src/types.ts.tpl +1 -1
- package/templates/minimal-app/apps/server/tsconfig.json.tpl +20 -14
- package/templates/minimal-app/apps/server/tsup.config.ts.tpl +9 -9
- package/templates/minimal-app/apps/shared/package.json.tpl +24 -24
- package/templates/minimal-app/apps/shared/src/modules/app/app.constants.ts.tpl +28 -2
- package/templates/minimal-app/apps/shared/src/modules/notification/notification.schema.ts.tpl +3 -3
- package/templates/minimal-app/apps/shared/src/modules/posts/posts.constants.ts.tpl +5 -5
- package/templates/minimal-app/apps/shared/src/modules/posts/posts.schema.ts.tpl +25 -76
- package/templates/minimal-app/apps/shared/tsconfig.json.tpl +16 -12
- package/templates/minimal-app/apps/webapp/index.html.tpl +22 -22
- package/templates/minimal-app/apps/webapp/package.json.tpl +53 -52
- package/templates/minimal-app/apps/webapp/public/{push-sw.js → push-sw.js.tpl} +34 -34
- package/templates/minimal-app/apps/webapp/src/App.tsx.tpl +44 -13
- package/templates/minimal-app/apps/webapp/src/Layout.tsx.tpl +18 -12
- package/templates/minimal-app/apps/webapp/src/Router.tsx.tpl +33 -40
- package/templates/minimal-app/apps/webapp/src/index.css.tpl +15 -75
- package/templates/minimal-app/apps/webapp/src/main.tsx.tpl +26 -26
- package/templates/minimal-app/apps/webapp/src/modules/posts/PostsRoute.tsx.tpl +278 -307
- package/templates/minimal-app/apps/webapp/src/modules/posts/components/PostCard.tsx.tpl +96 -138
- package/templates/minimal-app/apps/webapp/src/modules/posts/components/PostEditorModal.tsx.tpl +118 -124
- package/templates/minimal-app/apps/webapp/src/modules/posts/hooks/usePostActions.ts.tpl +108 -116
- package/templates/minimal-app/apps/webapp/src/modules/posts/hooks/usePostsList.ts.tpl +15 -9
- package/templates/minimal-app/apps/webapp/src/modules/posts/hooks/usePostsRoute.ts.tpl +13 -13
- package/templates/minimal-app/apps/webapp/src/modules/posts/posts.utils.ts.tpl +16 -16
- package/templates/minimal-app/apps/webapp/src/utils/i18n.ts.tpl +13 -13
- package/templates/minimal-app/apps/webapp/src/utils/trpc.ts.tpl +10 -6
- package/templates/minimal-app/apps/webapp/src/vite-env.d.ts.tpl +1 -1
- package/templates/minimal-app/apps/webapp/translations/{app.json.tpl → en/app.json.tpl} +7 -7
- package/templates/minimal-app/apps/webapp/translations/en/{blog-app.json.tpl → {{APP_SLUG}}.json.tpl} +135 -135
- package/templates/minimal-app/apps/webapp/tsconfig.json.tpl +17 -17
- package/templates/minimal-app/apps/webapp/vite.config.ts.tpl +31 -31
- package/templates/minimal-app/template.manifest.json +21 -0
- package/templates/minimal-app/apps/server/drizzle/generate-schema.ts.tpl +0 -31
- package/templates/minimal-app/apps/server/src/db.ts.tpl +0 -7
- package/templates/minimal-app/apps/server/src/generated/schema.ts.tpl +0 -13
- package/templates/minimal-app/apps/server/src/utils/trpc.ts.tpl +0 -8
- package/templates/minimal-app/apps/shared/src/modules/app/locale.constants.ts.tpl +0 -7
- package/templates/minimal-app/apps/shared/src/modules/app/roles.constants.ts.tpl +0 -17
- package/templates/minimal-app/apps/webapp/src/Providers.tsx.tpl +0 -36
- package/templates/minimal-app/apps/webapp/src/modules/notification/PushNotificationsPanel.tsx.tpl +0 -89
- package/templates/minimal-app/apps/webapp/src/navigation/navigation.config.tsx.tpl +0 -85
- /package/templates/minimal-app/apps/webapp/public/{logo.svg → logo.svg.tpl} +0 -0
package/dist/src/create.cjs
CHANGED
|
@@ -5,9 +5,16 @@ const require_src_strings = require("./strings.cjs");
|
|
|
5
5
|
const require_src_fs = require("./fs.cjs");
|
|
6
6
|
const require_src_paths = require("./paths.cjs");
|
|
7
7
|
const require_src_prompts = require("./prompts.cjs");
|
|
8
|
+
let node_fs = require("node:fs");
|
|
9
|
+
node_fs = require_runtime.__toESM(node_fs);
|
|
8
10
|
let node_path = require("node:path");
|
|
9
11
|
node_path = require_runtime.__toESM(node_path);
|
|
10
12
|
//#region src/create.ts
|
|
13
|
+
function loadFeatureManifest(templateDirectory) {
|
|
14
|
+
const manifestPath = node_path.default.join(templateDirectory, "template.manifest.json");
|
|
15
|
+
if (!node_fs.default.existsSync(manifestPath)) return { features: {} };
|
|
16
|
+
return JSON.parse(node_fs.default.readFileSync(manifestPath, "utf8"));
|
|
17
|
+
}
|
|
11
18
|
async function scaffoldProject(initialOptions) {
|
|
12
19
|
const options = await require_src_prompts.resolveCreateCommandOptions(initialOptions);
|
|
13
20
|
const appSlug = require_src_strings.slugifyAppName(options.appName);
|
|
@@ -20,8 +27,18 @@ async function scaffoldProject(initialOptions) {
|
|
|
20
27
|
packageScope: require_src_strings.derivePackageScope(appSlug),
|
|
21
28
|
betterAuthSecret: require_src_strings.createBetterAuthSecret()
|
|
22
29
|
};
|
|
30
|
+
const platform = options.platform ?? "web";
|
|
31
|
+
const enabledFeatures = /* @__PURE__ */ new Set();
|
|
32
|
+
if (platform !== "expo") enabledFeatures.add("webapp");
|
|
33
|
+
if (platform !== "web") enabledFeatures.add("expo");
|
|
34
|
+
if (options.testHarness) enabledFeatures.add("test-harness");
|
|
35
|
+
const manifest = loadFeatureManifest(templateDirectory);
|
|
36
|
+
const excludePrefixes = Object.entries(manifest.features).filter(([feature]) => !enabledFeatures.has(feature)).flatMap(([, config]) => config.paths);
|
|
23
37
|
await require_src_fs.ensureDirectoryState(targetDirectory, options.force);
|
|
24
|
-
await require_src_fs.copyTemplateDirectory(templateDirectory, targetDirectory, context
|
|
38
|
+
await require_src_fs.copyTemplateDirectory(templateDirectory, targetDirectory, context, {
|
|
39
|
+
excludePrefixes,
|
|
40
|
+
enabledFeatures
|
|
41
|
+
});
|
|
25
42
|
if (!options.skipGit) await require_src_fs.runGitInit(targetDirectory);
|
|
26
43
|
if (!options.skipInstall) await require_src_fs.runInstall(targetDirectory);
|
|
27
44
|
return {
|
package/dist/src/create.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"create.cjs","names":["
|
|
1
|
+
{"version":3,"file":"create.cjs","names":["path","fs","resolveCreateCommandOptions","slugifyAppName","getTemplateRoot","derivePackageScope","createBetterAuthSecret","ensureDirectoryState","copyTemplateDirectory","runGitInit","runInstall","TEMPLATE_NAME"],"sources":["../../src/create.ts"],"sourcesContent":["import fs from \"node:fs\";\nimport path from \"node:path\";\nimport { TEMPLATE_NAME } from \"./constants\";\nimport { copyTemplateDirectory, ensureDirectoryState, runGitInit, runInstall } from \"./fs\";\nimport { getTemplateRoot } from \"./paths\";\nimport { resolveCreateCommandOptions } from \"./prompts\";\nimport { createBetterAuthSecret, derivePackageScope, slugifyAppName } from \"./strings\";\nimport type {\n CreateCommandOptions,\n TemplateContext,\n TemplateFeatureManifest,\n} from \"./types\";\n\nexport interface ScaffoldResult {\n context: TemplateContext;\n targetDirectory: string;\n templateName: string;\n}\n\nfunction loadFeatureManifest(templateDirectory: string): TemplateFeatureManifest {\n const manifestPath = path.join(templateDirectory, \"template.manifest.json\");\n if (!fs.existsSync(manifestPath)) {\n return { features: {} };\n }\n return JSON.parse(fs.readFileSync(manifestPath, \"utf8\")) as TemplateFeatureManifest;\n}\n\nexport async function scaffoldProject(initialOptions: CreateCommandOptions): Promise<ScaffoldResult> {\n const options = await resolveCreateCommandOptions(initialOptions);\n const appSlug = slugifyAppName(options.appName!);\n const targetDirectory = path.resolve(process.cwd(), options.targetDirectory!);\n const templateDirectory = getTemplateRoot();\n\n const context: TemplateContext = {\n appName: options.appName!,\n appDescription: options.appDescription!,\n appSlug,\n packageScope: derivePackageScope(appSlug),\n betterAuthSecret: createBetterAuthSecret(),\n };\n\n const platform = options.platform ?? \"web\";\n const enabledFeatures = new Set<string>();\n if (platform !== \"expo\") enabledFeatures.add(\"webapp\");\n if (platform !== \"web\") enabledFeatures.add(\"expo\");\n if (options.testHarness) enabledFeatures.add(\"test-harness\");\n\n const manifest = loadFeatureManifest(templateDirectory);\n const excludePrefixes = Object.entries(manifest.features)\n .filter(([feature]) => !enabledFeatures.has(feature))\n .flatMap(([, config]) => config.paths);\n\n await ensureDirectoryState(targetDirectory, options.force);\n await copyTemplateDirectory(templateDirectory, targetDirectory, context, {\n excludePrefixes,\n enabledFeatures,\n });\n\n if (!options.skipGit) {\n await runGitInit(targetDirectory);\n }\n\n if (!options.skipInstall) {\n await runInstall(targetDirectory);\n }\n\n return {\n context,\n targetDirectory,\n templateName: TEMPLATE_NAME,\n };\n}\n"],"mappings":";;;;;;;;;;;;AAmBA,SAAS,oBAAoB,mBAAoD;CAC/E,MAAM,eAAeA,UAAAA,QAAK,KAAK,mBAAmB,yBAAyB;AAC3E,KAAI,CAACC,QAAAA,QAAG,WAAW,aAAa,CAC9B,QAAO,EAAE,UAAU,EAAE,EAAE;AAEzB,QAAO,KAAK,MAAMA,QAAAA,QAAG,aAAa,cAAc,OAAO,CAAC;;AAG1D,eAAsB,gBAAgB,gBAA+D;CACnG,MAAM,UAAU,MAAMC,oBAAAA,4BAA4B,eAAe;CACjE,MAAM,UAAUC,oBAAAA,eAAe,QAAQ,QAAS;CAChD,MAAM,kBAAkBH,UAAAA,QAAK,QAAQ,QAAQ,KAAK,EAAE,QAAQ,gBAAiB;CAC7E,MAAM,oBAAoBI,kBAAAA,iBAAiB;CAE3C,MAAM,UAA2B;EAC/B,SAAS,QAAQ;EACjB,gBAAgB,QAAQ;EACxB;EACA,cAAcC,oBAAAA,mBAAmB,QAAQ;EACzC,kBAAkBC,oBAAAA,wBAAwB;EAC3C;CAED,MAAM,WAAW,QAAQ,YAAY;CACrC,MAAM,kCAAkB,IAAI,KAAa;AACzC,KAAI,aAAa,OAAQ,iBAAgB,IAAI,SAAS;AACtD,KAAI,aAAa,MAAO,iBAAgB,IAAI,OAAO;AACnD,KAAI,QAAQ,YAAa,iBAAgB,IAAI,eAAe;CAE5D,MAAM,WAAW,oBAAoB,kBAAkB;CACvD,MAAM,kBAAkB,OAAO,QAAQ,SAAS,SAAS,CACtD,QAAQ,CAAC,aAAa,CAAC,gBAAgB,IAAI,QAAQ,CAAC,CACpD,SAAS,GAAG,YAAY,OAAO,MAAM;AAExC,OAAMC,eAAAA,qBAAqB,iBAAiB,QAAQ,MAAM;AAC1D,OAAMC,eAAAA,sBAAsB,mBAAmB,iBAAiB,SAAS;EACvE;EACA;EACD,CAAC;AAEF,KAAI,CAAC,QAAQ,QACX,OAAMC,eAAAA,WAAW,gBAAgB;AAGnC,KAAI,CAAC,QAAQ,YACX,OAAMC,eAAAA,WAAW,gBAAgB;AAGnC,QAAO;EACL;EACA;EACA,cAAcC,sBAAAA;EACf"}
|
package/dist/src/fs.cjs
CHANGED
|
@@ -8,7 +8,30 @@ let node_fs_promises = require("node:fs/promises");
|
|
|
8
8
|
node_fs_promises = require_runtime.__toESM(node_fs_promises);
|
|
9
9
|
//#region src/fs.ts
|
|
10
10
|
const execFileAsync = (0, require("node:util").promisify)(node_child_process.execFile);
|
|
11
|
-
const
|
|
11
|
+
const TEXT_EXTENSIONS = new Set([
|
|
12
|
+
".ts",
|
|
13
|
+
".tsx",
|
|
14
|
+
".js",
|
|
15
|
+
".mjs",
|
|
16
|
+
".cjs",
|
|
17
|
+
".json",
|
|
18
|
+
".md",
|
|
19
|
+
".css",
|
|
20
|
+
".html",
|
|
21
|
+
".svg",
|
|
22
|
+
".yaml",
|
|
23
|
+
".yml",
|
|
24
|
+
".txt",
|
|
25
|
+
".mdc"
|
|
26
|
+
]);
|
|
27
|
+
const TEXT_BASENAMES = new Set([
|
|
28
|
+
".gitignore",
|
|
29
|
+
".env",
|
|
30
|
+
".env.example",
|
|
31
|
+
".npmrc"
|
|
32
|
+
]);
|
|
33
|
+
/** Feature marker blocks: lines between `// m5k:<feature>:start` and `// m5k:<feature>:end`. */
|
|
34
|
+
const MARKER_PATTERN = /^[ \t]*\/\/ m5k:([a-z-]+):(start|end)[ \t]*$/;
|
|
12
35
|
async function ensureDirectoryState(targetDirectory, force) {
|
|
13
36
|
const stat = await node_fs_promises.default.stat(targetDirectory).catch(() => null);
|
|
14
37
|
if (!stat) {
|
|
@@ -18,20 +41,56 @@ async function ensureDirectoryState(targetDirectory, force) {
|
|
|
18
41
|
if (!stat.isDirectory()) throw new Error(`Target path is not a directory: ${targetDirectory}`);
|
|
19
42
|
if ((await node_fs_promises.default.readdir(targetDirectory)).length > 0 && !force) throw new Error(`Target directory is not empty: ${targetDirectory}. Re-run with --force to overwrite.`);
|
|
20
43
|
}
|
|
21
|
-
|
|
44
|
+
function isTextFile(fileName) {
|
|
45
|
+
const withoutTpl = fileName.endsWith(".tpl") ? fileName.slice(0, -4) : fileName;
|
|
46
|
+
if (TEXT_BASENAMES.has(withoutTpl)) return true;
|
|
47
|
+
return TEXT_EXTENSIONS.has(node_path.default.extname(withoutTpl));
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Removes feature marker lines. Blocks for disabled features are dropped with
|
|
51
|
+
* their content; blocks for enabled features keep the content, lose the markers.
|
|
52
|
+
*/
|
|
53
|
+
function applyFeatureMarkers(content, enabledFeatures) {
|
|
54
|
+
const lines = content.split("\n");
|
|
55
|
+
const out = [];
|
|
56
|
+
let skippingFeature;
|
|
57
|
+
for (const line of lines) {
|
|
58
|
+
const match = line.match(MARKER_PATTERN);
|
|
59
|
+
if (!match) {
|
|
60
|
+
if (skippingFeature === void 0) out.push(line);
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
const [, feature, edge] = match;
|
|
64
|
+
if (edge === "start") {
|
|
65
|
+
if (!enabledFeatures.has(feature)) skippingFeature = feature;
|
|
66
|
+
} else if (skippingFeature === feature) skippingFeature = void 0;
|
|
67
|
+
}
|
|
68
|
+
return out.join("\n");
|
|
69
|
+
}
|
|
70
|
+
async function copyTemplateDirectory(templateDirectory, targetDirectory, context, options = {}, relativeBase = "") {
|
|
22
71
|
const entries = await node_fs_promises.default.readdir(templateDirectory, { withFileTypes: true });
|
|
72
|
+
const excludePrefixes = options.excludePrefixes ?? [];
|
|
73
|
+
const enabledFeatures = options.enabledFeatures ?? /* @__PURE__ */ new Set();
|
|
23
74
|
for (const entry of entries) {
|
|
24
75
|
const sourcePath = node_path.default.join(templateDirectory, entry.name);
|
|
25
|
-
const targetName = entry.name.endsWith(".tpl") ? entry.name.slice(0, -4) : entry.name;
|
|
76
|
+
const targetName = require_src_strings.renderTemplate(entry.name.endsWith(".tpl") ? entry.name.slice(0, -4) : entry.name, context);
|
|
77
|
+
const relativePath = relativeBase ? `${relativeBase}/${targetName}` : targetName;
|
|
78
|
+
if (relativePath === "template.manifest.json") continue;
|
|
79
|
+
const asDirPrefix = `${relativePath}/`;
|
|
80
|
+
if (excludePrefixes.some((p) => relativePath === p || asDirPrefix === p || relativePath.startsWith(p))) continue;
|
|
26
81
|
const targetPath = node_path.default.join(targetDirectory, targetName);
|
|
27
82
|
if (entry.isDirectory()) {
|
|
28
83
|
await node_fs_promises.default.mkdir(targetPath, { recursive: true });
|
|
29
|
-
await copyTemplateDirectory(sourcePath, targetPath, context);
|
|
84
|
+
await copyTemplateDirectory(sourcePath, targetPath, context, options, relativePath);
|
|
30
85
|
continue;
|
|
31
86
|
}
|
|
32
87
|
if (!entry.isFile()) continue;
|
|
33
|
-
const rendered = require_src_strings.renderTemplate(await node_fs_promises.default.readFile(sourcePath, "utf8"), context).replace(/\r?\n/g, TEMPLATE_LINE_ENDING);
|
|
34
88
|
await node_fs_promises.default.mkdir(node_path.default.dirname(targetPath), { recursive: true });
|
|
89
|
+
if (!isTextFile(entry.name)) {
|
|
90
|
+
await node_fs_promises.default.copyFile(sourcePath, targetPath);
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
const rendered = applyFeatureMarkers(require_src_strings.renderTemplate(await node_fs_promises.default.readFile(sourcePath, "utf8"), context), enabledFeatures).replace(/\r\n?/g, "\n");
|
|
35
94
|
await node_fs_promises.default.writeFile(targetPath, rendered, "utf8");
|
|
36
95
|
}
|
|
37
96
|
}
|
|
@@ -48,6 +107,7 @@ async function runGitInit(targetDirectory) {
|
|
|
48
107
|
});
|
|
49
108
|
}
|
|
50
109
|
//#endregion
|
|
110
|
+
exports.applyFeatureMarkers = applyFeatureMarkers;
|
|
51
111
|
exports.copyTemplateDirectory = copyTemplateDirectory;
|
|
52
112
|
exports.ensureDirectoryState = ensureDirectoryState;
|
|
53
113
|
exports.runGitInit = runGitInit;
|
package/dist/src/fs.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fs.cjs","names":["execFile","fs","path","renderTemplate"],"sources":["../../src/fs.ts"],"sourcesContent":["import { execFile } from \"node:child_process\";\
|
|
1
|
+
{"version":3,"file":"fs.cjs","names":["execFile","fs","path","renderTemplate"],"sources":["../../src/fs.ts"],"sourcesContent":["import { execFile } from \"node:child_process\";\nimport fs from \"node:fs/promises\";\nimport path from \"node:path\";\nimport { promisify } from \"node:util\";\nimport type { TemplateContext } from \"./types\";\nimport { renderTemplate } from \"./strings\";\n\nconst execFileAsync = promisify(execFile);\n\nconst TEXT_EXTENSIONS = new Set([\n \".ts\", \".tsx\", \".js\", \".mjs\", \".cjs\", \".json\", \".md\", \".css\", \".html\",\n \".svg\", \".yaml\", \".yml\", \".txt\", \".mdc\",\n]);\nconst TEXT_BASENAMES = new Set([\".gitignore\", \".env\", \".env.example\", \".npmrc\"]);\n\n/** Feature marker blocks: lines between `// m5k:<feature>:start` and `// m5k:<feature>:end`. */\nconst MARKER_PATTERN = /^[ \\t]*\\/\\/ m5k:([a-z-]+):(start|end)[ \\t]*$/;\n\nexport interface CopyTemplateOptions {\n /** Repo-root-relative path prefixes to skip entirely (disabled features). */\n excludePrefixes?: readonly string[];\n /** Features whose marker blocks should be kept (markers removed, content kept). */\n enabledFeatures?: ReadonlySet<string>;\n}\n\nexport async function ensureDirectoryState(\n targetDirectory: string,\n force: boolean\n): Promise<void> {\n const stat = await fs.stat(targetDirectory).catch(() => null);\n\n if (!stat) {\n await fs.mkdir(targetDirectory, { recursive: true });\n return;\n }\n\n if (!stat.isDirectory()) {\n throw new Error(`Target path is not a directory: ${targetDirectory}`);\n }\n\n const entries = await fs.readdir(targetDirectory);\n if (entries.length > 0 && !force) {\n throw new Error(\n `Target directory is not empty: ${targetDirectory}. Re-run with --force to overwrite.`\n );\n }\n}\n\nfunction isTextFile(fileName: string): boolean {\n const withoutTpl = fileName.endsWith(\".tpl\") ? fileName.slice(0, -4) : fileName;\n if (TEXT_BASENAMES.has(withoutTpl)) return true;\n return TEXT_EXTENSIONS.has(path.extname(withoutTpl));\n}\n\n/**\n * Removes feature marker lines. Blocks for disabled features are dropped with\n * their content; blocks for enabled features keep the content, lose the markers.\n */\nexport function applyFeatureMarkers(content: string, enabledFeatures: ReadonlySet<string>): string {\n const lines = content.split(\"\\n\");\n const out: string[] = [];\n let skippingFeature: string | undefined;\n\n for (const line of lines) {\n const match = line.match(MARKER_PATTERN);\n if (!match) {\n if (skippingFeature === undefined) out.push(line);\n continue;\n }\n\n const [, feature, edge] = match;\n if (edge === \"start\") {\n if (!enabledFeatures.has(feature)) skippingFeature = feature;\n } else if (skippingFeature === feature) {\n skippingFeature = undefined;\n }\n // marker lines themselves are never emitted\n }\n\n return out.join(\"\\n\");\n}\n\nexport async function copyTemplateDirectory(\n templateDirectory: string,\n targetDirectory: string,\n context: TemplateContext,\n options: CopyTemplateOptions = {},\n relativeBase = \"\"\n): Promise<void> {\n const entries = await fs.readdir(templateDirectory, { withFileTypes: true });\n const excludePrefixes = options.excludePrefixes ?? [];\n const enabledFeatures = options.enabledFeatures ?? new Set<string>();\n\n for (const entry of entries) {\n const sourcePath = path.join(templateDirectory, entry.name);\n const targetName = renderTemplate(\n entry.name.endsWith(\".tpl\") ? entry.name.slice(0, -4) : entry.name,\n context\n );\n const relativePath = relativeBase ? `${relativeBase}/${targetName}` : targetName;\n\n if (relativePath === \"template.manifest.json\") {\n continue;\n }\n\n const asDirPrefix = `${relativePath}/`;\n if (excludePrefixes.some((p) => relativePath === p || asDirPrefix === p || relativePath.startsWith(p))) {\n continue;\n }\n\n const targetPath = path.join(targetDirectory, targetName);\n\n if (entry.isDirectory()) {\n await fs.mkdir(targetPath, { recursive: true });\n await copyTemplateDirectory(sourcePath, targetPath, context, options, relativePath);\n continue;\n }\n\n if (!entry.isFile()) {\n continue;\n }\n\n await fs.mkdir(path.dirname(targetPath), { recursive: true });\n\n if (!isTextFile(entry.name)) {\n await fs.copyFile(sourcePath, targetPath);\n continue;\n }\n\n const content = await fs.readFile(sourcePath, \"utf8\");\n const rendered = applyFeatureMarkers(\n renderTemplate(content, context),\n enabledFeatures\n ).replace(/\\r\\n?/g, \"\\n\");\n await fs.writeFile(targetPath, rendered, \"utf8\");\n }\n}\n\nexport async function runInstall(targetDirectory: string): Promise<void> {\n await execFileAsync(\"pnpm\", [\"install\"], {\n cwd: targetDirectory,\n env: process.env,\n });\n}\n\nexport async function runGitInit(targetDirectory: string): Promise<void> {\n await execFileAsync(\"git\", [\"init\"], {\n cwd: targetDirectory,\n env: process.env,\n });\n}\n"],"mappings":";;;;;;;;;AAOA,MAAM,iBAAA,wBAAA,WAA0BA,mBAAAA,SAAS;AAEzC,MAAM,kBAAkB,IAAI,IAAI;CAC9B;CAAO;CAAQ;CAAO;CAAQ;CAAQ;CAAS;CAAO;CAAQ;CAC9D;CAAQ;CAAS;CAAQ;CAAQ;CAClC,CAAC;AACF,MAAM,iBAAiB,IAAI,IAAI;CAAC;CAAc;CAAQ;CAAgB;CAAS,CAAC;;AAGhF,MAAM,iBAAiB;AASvB,eAAsB,qBACpB,iBACA,OACe;CACf,MAAM,OAAO,MAAMC,iBAAAA,QAAG,KAAK,gBAAgB,CAAC,YAAY,KAAK;AAE7D,KAAI,CAAC,MAAM;AACT,QAAMA,iBAAAA,QAAG,MAAM,iBAAiB,EAAE,WAAW,MAAM,CAAC;AACpD;;AAGF,KAAI,CAAC,KAAK,aAAa,CACrB,OAAM,IAAI,MAAM,mCAAmC,kBAAkB;AAIvE,MADgB,MAAMA,iBAAAA,QAAG,QAAQ,gBAAgB,EACrC,SAAS,KAAK,CAAC,MACzB,OAAM,IAAI,MACR,kCAAkC,gBAAgB,qCACnD;;AAIL,SAAS,WAAW,UAA2B;CAC7C,MAAM,aAAa,SAAS,SAAS,OAAO,GAAG,SAAS,MAAM,GAAG,GAAG,GAAG;AACvE,KAAI,eAAe,IAAI,WAAW,CAAE,QAAO;AAC3C,QAAO,gBAAgB,IAAIC,UAAAA,QAAK,QAAQ,WAAW,CAAC;;;;;;AAOtD,SAAgB,oBAAoB,SAAiB,iBAA8C;CACjG,MAAM,QAAQ,QAAQ,MAAM,KAAK;CACjC,MAAM,MAAgB,EAAE;CACxB,IAAI;AAEJ,MAAK,MAAM,QAAQ,OAAO;EACxB,MAAM,QAAQ,KAAK,MAAM,eAAe;AACxC,MAAI,CAAC,OAAO;AACV,OAAI,oBAAoB,KAAA,EAAW,KAAI,KAAK,KAAK;AACjD;;EAGF,MAAM,GAAG,SAAS,QAAQ;AAC1B,MAAI,SAAS;OACP,CAAC,gBAAgB,IAAI,QAAQ,CAAE,mBAAkB;aAC5C,oBAAoB,QAC7B,mBAAkB,KAAA;;AAKtB,QAAO,IAAI,KAAK,KAAK;;AAGvB,eAAsB,sBACpB,mBACA,iBACA,SACA,UAA+B,EAAE,EACjC,eAAe,IACA;CACf,MAAM,UAAU,MAAMD,iBAAAA,QAAG,QAAQ,mBAAmB,EAAE,eAAe,MAAM,CAAC;CAC5E,MAAM,kBAAkB,QAAQ,mBAAmB,EAAE;CACrD,MAAM,kBAAkB,QAAQ,mCAAmB,IAAI,KAAa;AAEpE,MAAK,MAAM,SAAS,SAAS;EAC3B,MAAM,aAAaC,UAAAA,QAAK,KAAK,mBAAmB,MAAM,KAAK;EAC3D,MAAM,aAAaC,oBAAAA,eACjB,MAAM,KAAK,SAAS,OAAO,GAAG,MAAM,KAAK,MAAM,GAAG,GAAG,GAAG,MAAM,MAC9D,QACD;EACD,MAAM,eAAe,eAAe,GAAG,aAAa,GAAG,eAAe;AAEtE,MAAI,iBAAiB,yBACnB;EAGF,MAAM,cAAc,GAAG,aAAa;AACpC,MAAI,gBAAgB,MAAM,MAAM,iBAAiB,KAAK,gBAAgB,KAAK,aAAa,WAAW,EAAE,CAAC,CACpG;EAGF,MAAM,aAAaD,UAAAA,QAAK,KAAK,iBAAiB,WAAW;AAEzD,MAAI,MAAM,aAAa,EAAE;AACvB,SAAMD,iBAAAA,QAAG,MAAM,YAAY,EAAE,WAAW,MAAM,CAAC;AAC/C,SAAM,sBAAsB,YAAY,YAAY,SAAS,SAAS,aAAa;AACnF;;AAGF,MAAI,CAAC,MAAM,QAAQ,CACjB;AAGF,QAAMA,iBAAAA,QAAG,MAAMC,UAAAA,QAAK,QAAQ,WAAW,EAAE,EAAE,WAAW,MAAM,CAAC;AAE7D,MAAI,CAAC,WAAW,MAAM,KAAK,EAAE;AAC3B,SAAMD,iBAAAA,QAAG,SAAS,YAAY,WAAW;AACzC;;EAIF,MAAM,WAAW,oBACfE,oBAAAA,eAFc,MAAMF,iBAAAA,QAAG,SAAS,YAAY,OAAO,EAE3B,QAAQ,EAChC,gBACD,CAAC,QAAQ,UAAU,KAAK;AACzB,QAAMA,iBAAAA,QAAG,UAAU,YAAY,UAAU,OAAO;;;AAIpD,eAAsB,WAAW,iBAAwC;AACvE,OAAM,cAAc,QAAQ,CAAC,UAAU,EAAE;EACvC,KAAK;EACL,KAAK,QAAQ;EACd,CAAC;;AAGJ,eAAsB,WAAW,iBAAwC;AACvE,OAAM,cAAc,OAAO,CAAC,OAAO,EAAE;EACnC,KAAK;EACL,KAAK,QAAQ;EACd,CAAC"}
|
package/dist/src/fs.d.cts
CHANGED
|
@@ -1,10 +1,21 @@
|
|
|
1
1
|
import { TemplateContext } from "./types.cjs";
|
|
2
2
|
|
|
3
3
|
//#region src/fs.d.ts
|
|
4
|
+
interface CopyTemplateOptions {
|
|
5
|
+
/** Repo-root-relative path prefixes to skip entirely (disabled features). */
|
|
6
|
+
excludePrefixes?: readonly string[];
|
|
7
|
+
/** Features whose marker blocks should be kept (markers removed, content kept). */
|
|
8
|
+
enabledFeatures?: ReadonlySet<string>;
|
|
9
|
+
}
|
|
4
10
|
declare function ensureDirectoryState(targetDirectory: string, force: boolean): Promise<void>;
|
|
5
|
-
|
|
11
|
+
/**
|
|
12
|
+
* Removes feature marker lines. Blocks for disabled features are dropped with
|
|
13
|
+
* their content; blocks for enabled features keep the content, lose the markers.
|
|
14
|
+
*/
|
|
15
|
+
declare function applyFeatureMarkers(content: string, enabledFeatures: ReadonlySet<string>): string;
|
|
16
|
+
declare function copyTemplateDirectory(templateDirectory: string, targetDirectory: string, context: TemplateContext, options?: CopyTemplateOptions, relativeBase?: string): Promise<void>;
|
|
6
17
|
declare function runInstall(targetDirectory: string): Promise<void>;
|
|
7
18
|
declare function runGitInit(targetDirectory: string): Promise<void>;
|
|
8
19
|
//#endregion
|
|
9
|
-
export { copyTemplateDirectory, ensureDirectoryState, runGitInit, runInstall };
|
|
20
|
+
export { CopyTemplateOptions, applyFeatureMarkers, copyTemplateDirectory, ensureDirectoryState, runGitInit, runInstall };
|
|
10
21
|
//# sourceMappingURL=fs.d.cts.map
|
package/dist/src/index.cjs
CHANGED
|
File without changes
|
package/dist/src/paths.cjs
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
2
|
const require_runtime = require("../_virtual/_rolldown/runtime.cjs");
|
|
3
3
|
const require_src_constants = require("./constants.cjs");
|
|
4
|
-
let node_path = require("node:path");
|
|
5
|
-
node_path = require_runtime.__toESM(node_path);
|
|
6
4
|
let node_fs = require("node:fs");
|
|
7
5
|
node_fs = require_runtime.__toESM(node_fs);
|
|
6
|
+
let node_path = require("node:path");
|
|
7
|
+
node_path = require_runtime.__toESM(node_path);
|
|
8
8
|
//#region src/paths.ts
|
|
9
9
|
function getTemplateRoot() {
|
|
10
10
|
const sourcePath = node_path.default.resolve(__dirname, "../templates", require_src_constants.TEMPLATE_NAME);
|
package/dist/src/prompts.cjs
CHANGED
|
@@ -21,6 +21,10 @@ async function promptValue(question, fallback) {
|
|
|
21
21
|
rl.close();
|
|
22
22
|
}
|
|
23
23
|
}
|
|
24
|
+
function parsePlatform(value) {
|
|
25
|
+
const normalized = value.trim().toLowerCase();
|
|
26
|
+
if (normalized === "web" || normalized === "expo" || normalized === "both") return normalized;
|
|
27
|
+
}
|
|
24
28
|
async function resolveCreateCommandOptions(options) {
|
|
25
29
|
const resolved = { ...options };
|
|
26
30
|
if (!resolved.appName) {
|
|
@@ -37,6 +41,24 @@ async function resolveCreateCommandOptions(options) {
|
|
|
37
41
|
const fallback = require_src_constants.getDefaultDescription(resolved.appName);
|
|
38
42
|
resolved.appDescription = resolved.yes ? fallback : await promptValue("App description", fallback);
|
|
39
43
|
}
|
|
44
|
+
if (!resolved.platform) {
|
|
45
|
+
requireInteractive(resolved.yes);
|
|
46
|
+
if (resolved.yes) resolved.platform = "web";
|
|
47
|
+
else {
|
|
48
|
+
const answer = await promptValue("App platform — web, expo, or both", "web");
|
|
49
|
+
const platform = parsePlatform(answer);
|
|
50
|
+
if (!platform) throw new Error(`Invalid platform "${answer}". Use web, expo, or both.`);
|
|
51
|
+
resolved.platform = platform;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
if (resolved.testHarness === void 0) {
|
|
55
|
+
requireInteractive(resolved.yes);
|
|
56
|
+
if (resolved.yes) resolved.testHarness = false;
|
|
57
|
+
else {
|
|
58
|
+
const answer = await promptValue("Include the e2e test harness? y/N", "n");
|
|
59
|
+
resolved.testHarness = /^y(es)?$/i.test(answer.trim());
|
|
60
|
+
}
|
|
61
|
+
}
|
|
40
62
|
return resolved;
|
|
41
63
|
}
|
|
42
64
|
function pathBaseName(value) {
|
package/dist/src/prompts.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"prompts.cjs","names":["readline","toDisplayName","DEFAULT_APP_NAME","slugifyAppName","getDefaultDescription"],"sources":["../../src/prompts.ts"],"sourcesContent":["import readline from \"node:readline/promises\";\
|
|
1
|
+
{"version":3,"file":"prompts.cjs","names":["readline","toDisplayName","DEFAULT_APP_NAME","slugifyAppName","getDefaultDescription"],"sources":["../../src/prompts.ts"],"sourcesContent":["import readline from \"node:readline/promises\";\nimport { stdin as input, stdout as output } from \"node:process\";\nimport { DEFAULT_APP_NAME, getDefaultDescription } from \"./constants\";\nimport { slugifyAppName, toDisplayName } from \"./strings\";\nimport type { AppPlatform, CreateCommandOptions } from \"./types\";\n\nfunction requireInteractive(yes: boolean): void {\n if (!yes && !process.stdin.isTTY) {\n throw new Error(\"Missing required values in a non-interactive shell. Pass --yes or provide flags.\");\n }\n}\n\nasync function promptValue(question: string, fallback?: string): Promise<string> {\n const rl = readline.createInterface({ input, output });\n\n try {\n const suffix = fallback ? ` (${fallback})` : \"\";\n const response = await rl.question(`${question}${suffix}: `);\n return response.trim() || fallback || \"\";\n } finally {\n rl.close();\n }\n}\n\nfunction parsePlatform(value: string): AppPlatform | undefined {\n const normalized = value.trim().toLowerCase();\n if (normalized === \"web\" || normalized === \"expo\" || normalized === \"both\") {\n return normalized;\n }\n return undefined;\n}\n\nexport async function resolveCreateCommandOptions(\n options: CreateCommandOptions\n): Promise<CreateCommandOptions> {\n const resolved = { ...options };\n\n if (!resolved.appName) {\n requireInteractive(resolved.yes);\n resolved.appName = resolved.yes\n ? resolved.targetDirectory\n ? toDisplayName(pathBaseName(resolved.targetDirectory))\n : DEFAULT_APP_NAME\n : await promptValue(\"App name\", resolved.targetDirectory ? pathBaseName(resolved.targetDirectory) : DEFAULT_APP_NAME);\n }\n\n resolved.appName = toDisplayName(resolved.appName);\n\n if (!resolved.targetDirectory) {\n requireInteractive(resolved.yes);\n resolved.targetDirectory = resolved.yes\n ? slugifyAppName(resolved.appName)\n : await promptValue(\"Target directory\", slugifyAppName(resolved.appName));\n }\n\n if (!resolved.appDescription) {\n requireInteractive(resolved.yes);\n const fallback = getDefaultDescription(resolved.appName);\n resolved.appDescription = resolved.yes\n ? fallback\n : await promptValue(\"App description\", fallback);\n }\n\n if (!resolved.platform) {\n requireInteractive(resolved.yes);\n if (resolved.yes) {\n resolved.platform = \"web\";\n } else {\n const answer = await promptValue(\"App platform — web, expo, or both\", \"web\");\n const platform = parsePlatform(answer);\n if (!platform) {\n throw new Error(`Invalid platform \"${answer}\". Use web, expo, or both.`);\n }\n resolved.platform = platform;\n }\n }\n\n if (resolved.testHarness === undefined) {\n requireInteractive(resolved.yes);\n if (resolved.yes) {\n resolved.testHarness = false;\n } else {\n const answer = await promptValue(\"Include the e2e test harness? y/N\", \"n\");\n resolved.testHarness = /^y(es)?$/i.test(answer.trim());\n }\n }\n\n return resolved;\n}\n\nfunction pathBaseName(value: string): string {\n const normalized = value.replace(/[\\\\/]+$/g, \"\");\n const segments = normalized.split(/[\\\\/]/).filter(Boolean);\n return segments.at(-1) || DEFAULT_APP_NAME;\n}\n"],"mappings":";;;;;;;;AAMA,SAAS,mBAAmB,KAAoB;AAC9C,KAAI,CAAC,OAAO,CAAC,QAAQ,MAAM,MACzB,OAAM,IAAI,MAAM,mFAAmF;;AAIvG,eAAe,YAAY,UAAkB,UAAoC;CAC/E,MAAM,KAAKA,uBAAAA,QAAS,gBAAgB;EAAE,OAAA,aAAA;EAAO,QAAA,aAAA;EAAQ,CAAC;AAEtD,KAAI;EACF,MAAM,SAAS,WAAW,KAAK,SAAS,KAAK;AAE7C,UADiB,MAAM,GAAG,SAAS,GAAG,WAAW,OAAO,IAAI,EAC5C,MAAM,IAAI,YAAY;WAC9B;AACR,KAAG,OAAO;;;AAId,SAAS,cAAc,OAAwC;CAC7D,MAAM,aAAa,MAAM,MAAM,CAAC,aAAa;AAC7C,KAAI,eAAe,SAAS,eAAe,UAAU,eAAe,OAClE,QAAO;;AAKX,eAAsB,4BACpB,SAC+B;CAC/B,MAAM,WAAW,EAAE,GAAG,SAAS;AAE/B,KAAI,CAAC,SAAS,SAAS;AACrB,qBAAmB,SAAS,IAAI;AAChC,WAAS,UAAU,SAAS,MACxB,SAAS,kBACPC,oBAAAA,cAAc,aAAa,SAAS,gBAAgB,CAAC,GACrDC,sBAAAA,mBACF,MAAM,YAAY,YAAY,SAAS,kBAAkB,aAAa,SAAS,gBAAgB,GAAGA,sBAAAA,iBAAiB;;AAGzH,UAAS,UAAUD,oBAAAA,cAAc,SAAS,QAAQ;AAElD,KAAI,CAAC,SAAS,iBAAiB;AAC7B,qBAAmB,SAAS,IAAI;AAChC,WAAS,kBAAkB,SAAS,MAChCE,oBAAAA,eAAe,SAAS,QAAQ,GAChC,MAAM,YAAY,oBAAoBA,oBAAAA,eAAe,SAAS,QAAQ,CAAC;;AAG7E,KAAI,CAAC,SAAS,gBAAgB;AAC5B,qBAAmB,SAAS,IAAI;EAChC,MAAM,WAAWC,sBAAAA,sBAAsB,SAAS,QAAQ;AACxD,WAAS,iBAAiB,SAAS,MAC/B,WACA,MAAM,YAAY,mBAAmB,SAAS;;AAGpD,KAAI,CAAC,SAAS,UAAU;AACtB,qBAAmB,SAAS,IAAI;AAChC,MAAI,SAAS,IACX,UAAS,WAAW;OACf;GACL,MAAM,SAAS,MAAM,YAAY,qCAAqC,MAAM;GAC5E,MAAM,WAAW,cAAc,OAAO;AACtC,OAAI,CAAC,SACH,OAAM,IAAI,MAAM,qBAAqB,OAAO,4BAA4B;AAE1E,YAAS,WAAW;;;AAIxB,KAAI,SAAS,gBAAgB,KAAA,GAAW;AACtC,qBAAmB,SAAS,IAAI;AAChC,MAAI,SAAS,IACX,UAAS,cAAc;OAClB;GACL,MAAM,SAAS,MAAM,YAAY,qCAAqC,IAAI;AAC1E,YAAS,cAAc,YAAY,KAAK,OAAO,MAAM,CAAC;;;AAI1D,QAAO;;AAGT,SAAS,aAAa,OAAuB;AAG3C,QAFmB,MAAM,QAAQ,YAAY,GAAG,CACpB,MAAM,QAAQ,CAAC,OAAO,QAAQ,CAC1C,GAAG,GAAG,IAAA"}
|
package/dist/src/runCli.cjs
CHANGED
|
@@ -16,7 +16,7 @@ async function runCli(argv) {
|
|
|
16
16
|
console.log("Next steps:");
|
|
17
17
|
console.log(` cd ${result.targetDirectory}`);
|
|
18
18
|
if (options.skipInstall) console.log(" pnpm install");
|
|
19
|
-
console.log(" pnpm --filter ./apps/server
|
|
19
|
+
console.log(" pnpm --filter ./apps/server drizzle:migrate");
|
|
20
20
|
console.log(" pnpm dev");
|
|
21
21
|
}
|
|
22
22
|
function parseCli(argv) {
|
|
@@ -62,14 +62,23 @@ function isBooleanFlag(value) {
|
|
|
62
62
|
"yes",
|
|
63
63
|
"force",
|
|
64
64
|
"skip-install",
|
|
65
|
-
"skip-git"
|
|
65
|
+
"skip-git",
|
|
66
|
+
"with-test-harness"
|
|
66
67
|
].includes(value);
|
|
67
68
|
}
|
|
68
69
|
function toCreateCommandOptions(parsed) {
|
|
70
|
+
const platform = getStringOption(parsed.options, "platform");
|
|
71
|
+
if (platform && ![
|
|
72
|
+
"web",
|
|
73
|
+
"expo",
|
|
74
|
+
"both"
|
|
75
|
+
].includes(platform)) throw new Error(`Invalid --platform "${platform}". Use web, expo, or both.`);
|
|
69
76
|
return {
|
|
70
77
|
targetDirectory: parsed.directory,
|
|
71
78
|
appName: getStringOption(parsed.options, "name"),
|
|
72
79
|
appDescription: getStringOption(parsed.options, "description"),
|
|
80
|
+
platform,
|
|
81
|
+
testHarness: parsed.options["with-test-harness"] ? true : void 0,
|
|
73
82
|
yes: Boolean(parsed.options.yes),
|
|
74
83
|
force: Boolean(parsed.options.force),
|
|
75
84
|
skipInstall: Boolean(parsed.options["skip-install"]),
|
|
@@ -91,6 +100,8 @@ function printHelp() {
|
|
|
91
100
|
console.log("Options:");
|
|
92
101
|
console.log(" --name <value> Set the app name");
|
|
93
102
|
console.log(" --description <value> Set the app description");
|
|
103
|
+
console.log(" --platform <value> App platform: web (default), expo, or both");
|
|
104
|
+
console.log(" --with-test-harness Include the e2e test harness (default: no)");
|
|
94
105
|
console.log(" --yes Accept defaults for missing prompts");
|
|
95
106
|
console.log(" --force Allow writing into a non-empty directory");
|
|
96
107
|
console.log(" --skip-install Skip pnpm install");
|
package/dist/src/runCli.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"runCli.cjs","names":["scaffoldProject"],"sources":["../../src/runCli.ts"],"sourcesContent":["import { scaffoldProject } from \"./create\";\r\nimport type { CreateCommandOptions } from \"./types\";\r\n\r\ninterface ParsedCli {\r\n command?: string;\r\n directory?: string;\r\n help?: boolean;\r\n options: Record<string, string | boolean>;\r\n}\r\n\r\nexport async function runCli(argv: readonly string[]): Promise<void> {\r\n const parsed = parseCli(argv);\r\n\r\n if (\r\n !parsed.command ||\r\n parsed.help ||\r\n parsed.command === \"--help\" ||\r\n parsed.command === \"-h\" ||\r\n parsed.command === \"help\"\r\n ) {\r\n printHelp();\r\n return;\r\n }\r\n\r\n if (parsed.command !== \"create\") {\r\n throw new Error(`Unknown command: ${parsed.command}`);\r\n }\r\n\r\n const options = toCreateCommandOptions(parsed);\r\n const result = await scaffoldProject(options);\r\n\r\n console.log(\"\");\r\n console.log(`Scaffolded ${result.context.appName} in ${result.targetDirectory}`);\r\n console.log(\"\");\r\n console.log(\"Next steps:\");\r\n console.log(` cd ${result.targetDirectory}`);\r\n if (options.skipInstall) {\r\n console.log(\" pnpm install\");\r\n }\r\n console.log(\" pnpm --filter ./apps/server
|
|
1
|
+
{"version":3,"file":"runCli.cjs","names":["scaffoldProject"],"sources":["../../src/runCli.ts"],"sourcesContent":["import { scaffoldProject } from \"./create\";\r\nimport type { CreateCommandOptions } from \"./types\";\r\n\r\ninterface ParsedCli {\r\n command?: string;\r\n directory?: string;\r\n help?: boolean;\r\n options: Record<string, string | boolean>;\r\n}\r\n\r\nexport async function runCli(argv: readonly string[]): Promise<void> {\r\n const parsed = parseCli(argv);\r\n\r\n if (\r\n !parsed.command ||\r\n parsed.help ||\r\n parsed.command === \"--help\" ||\r\n parsed.command === \"-h\" ||\r\n parsed.command === \"help\"\r\n ) {\r\n printHelp();\r\n return;\r\n }\r\n\r\n if (parsed.command !== \"create\") {\r\n throw new Error(`Unknown command: ${parsed.command}`);\r\n }\r\n\r\n const options = toCreateCommandOptions(parsed);\r\n const result = await scaffoldProject(options);\r\n\r\n console.log(\"\");\r\n console.log(`Scaffolded ${result.context.appName} in ${result.targetDirectory}`);\r\n console.log(\"\");\r\n console.log(\"Next steps:\");\r\n console.log(` cd ${result.targetDirectory}`);\r\n if (options.skipInstall) {\r\n console.log(\" pnpm install\");\r\n }\r\n console.log(\" pnpm --filter ./apps/server drizzle:migrate\");\r\n console.log(\" pnpm dev\");\r\n}\r\n\r\nexport function parseCli(argv: readonly string[]): ParsedCli {\r\n const normalizedArgv = normalizeCliArgv(argv);\r\n const [command, ...rest] = normalizedArgv;\r\n const options: Record<string, string | boolean> = {};\r\n let directory: string | undefined;\r\n let help = false;\r\n\r\n for (let index = 0; index < rest.length; index += 1) {\r\n const token = rest[index];\r\n\r\n if (token === \"--help\" || token === \"-h\") {\r\n help = true;\r\n continue;\r\n }\r\n\r\n if (!token.startsWith(\"--\")) {\r\n directory ??= token;\r\n continue;\r\n }\r\n\r\n const [rawKey, inlineValue] = token.slice(2).split(\"=\", 2);\r\n\r\n if (isBooleanFlag(rawKey)) {\r\n options[rawKey] = true;\r\n continue;\r\n }\r\n\r\n const nextValue = inlineValue ?? rest[index + 1];\r\n if (!nextValue || nextValue.startsWith(\"--\")) {\r\n throw new Error(`Missing value for --${rawKey}`);\r\n }\r\n\r\n options[rawKey] = nextValue;\r\n if (inlineValue === undefined) {\r\n index += 1;\r\n }\r\n }\r\n\r\n return { command, directory, help, options };\r\n}\r\n\r\nfunction normalizeCliArgv(argv: readonly string[]): readonly string[] {\r\n const [firstToken] = argv;\r\n\r\n if (!firstToken || firstToken === \"create\" || firstToken === \"--help\" || firstToken === \"-h\") {\r\n return argv;\r\n }\r\n\r\n if (firstToken === \"help\") {\r\n return [\"--help\"];\r\n }\r\n\r\n return [\"create\", ...argv];\r\n}\r\n\r\nfunction isBooleanFlag(value: string): boolean {\r\n return [\"yes\", \"force\", \"skip-install\", \"skip-git\", \"with-test-harness\"].includes(value);\r\n}\r\n\r\nfunction toCreateCommandOptions(parsed: ParsedCli): CreateCommandOptions {\r\n const platform = getStringOption(parsed.options, \"platform\");\r\n if (platform && ![\"web\", \"expo\", \"both\"].includes(platform)) {\r\n throw new Error(`Invalid --platform \"${platform}\". Use web, expo, or both.`);\r\n }\r\n\r\n return {\r\n targetDirectory: parsed.directory,\r\n appName: getStringOption(parsed.options, \"name\"),\r\n appDescription: getStringOption(parsed.options, \"description\"),\r\n platform: platform as CreateCommandOptions[\"platform\"],\r\n testHarness: parsed.options[\"with-test-harness\"] ? true : undefined,\r\n yes: Boolean(parsed.options.yes),\r\n force: Boolean(parsed.options.force),\r\n skipInstall: Boolean(parsed.options[\"skip-install\"]),\r\n skipGit: Boolean(parsed.options[\"skip-git\"]),\r\n };\r\n}\r\n\r\nfunction getStringOption(\r\n options: Record<string, string | boolean>,\r\n key: string\r\n): string | undefined {\r\n const value = options[key];\r\n return typeof value === \"string\" ? value : undefined;\r\n}\r\n\r\nfunction printHelp(): void {\r\n console.log(\"m5kdev\");\r\n console.log(\"\");\r\n console.log(\"Usage:\");\r\n console.log(\" pnpm create m5kdev [directory] [options]\");\r\n console.log(\" m5kdev [directory] [options]\");\r\n console.log(\" m5kdev create [directory] [options]\");\r\n console.log(\"\");\r\n console.log(\"Options:\");\r\n console.log(\" --name <value> Set the app name\");\r\n console.log(\" --description <value> Set the app description\");\r\n console.log(\" --platform <value> App platform: web (default), expo, or both\");\r\n console.log(\" --with-test-harness Include the e2e test harness (default: no)\");\r\n console.log(\" --yes Accept defaults for missing prompts\");\r\n console.log(\" --force Allow writing into a non-empty directory\");\r\n console.log(\" --skip-install Skip pnpm install\");\r\n console.log(\" --skip-git Skip git init\");\r\n}\r\n"],"mappings":"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"}
|
package/dist/src/types.d.cts
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
//#region src/types.d.ts
|
|
2
|
+
type AppPlatform = "web" | "expo" | "both";
|
|
2
3
|
interface PromptValues {
|
|
3
4
|
targetDirectory?: string;
|
|
4
5
|
appName?: string;
|
|
5
6
|
appDescription?: string;
|
|
7
|
+
platform?: AppPlatform;
|
|
8
|
+
testHarness?: boolean;
|
|
6
9
|
}
|
|
7
10
|
interface CreateCommandOptions extends PromptValues {
|
|
8
11
|
yes: boolean;
|
|
@@ -17,6 +20,11 @@ interface TemplateContext {
|
|
|
17
20
|
packageScope: string;
|
|
18
21
|
betterAuthSecret: string;
|
|
19
22
|
}
|
|
23
|
+
interface TemplateFeatureManifest {
|
|
24
|
+
features: Record<string, {
|
|
25
|
+
paths: readonly string[];
|
|
26
|
+
}>;
|
|
27
|
+
}
|
|
20
28
|
//#endregion
|
|
21
|
-
export { CreateCommandOptions, PromptValues, TemplateContext };
|
|
29
|
+
export { AppPlatform, CreateCommandOptions, PromptValues, TemplateContext, TemplateFeatureManifest };
|
|
22
30
|
//# sourceMappingURL=types.d.cts.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-m5kdev",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.26.1",
|
|
4
4
|
"license": "GPL-3.0-only",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -13,8 +13,8 @@
|
|
|
13
13
|
"templates"
|
|
14
14
|
],
|
|
15
15
|
"bin": {
|
|
16
|
-
"create-m5kdev": "dist/src/index.
|
|
17
|
-
"m5kdev": "dist/src/index.
|
|
16
|
+
"create-m5kdev": "dist/src/index.cjs",
|
|
17
|
+
"m5kdev": "dist/src/index.cjs"
|
|
18
18
|
},
|
|
19
19
|
"dependencies": {
|
|
20
20
|
"tslib": "2.8.1"
|
|
@@ -25,14 +25,16 @@
|
|
|
25
25
|
"jest": "30.1.3",
|
|
26
26
|
"ts-jest": "29.4.9",
|
|
27
27
|
"tsdown": "0.21.7",
|
|
28
|
-
"typescript": "6.0.3"
|
|
28
|
+
"typescript": "6.0.3",
|
|
29
|
+
"tsx": "4.19.2"
|
|
29
30
|
},
|
|
30
31
|
"scripts": {
|
|
31
|
-
"build": "tsdown",
|
|
32
|
+
"build": "tsx scripts/prepare-templates.ts && tsdown",
|
|
32
33
|
"lint": "biome check .",
|
|
33
34
|
"lint:fix": "biome check . --write",
|
|
34
35
|
"check-types": "tsc --noEmit",
|
|
35
36
|
"test": "jest -c jest.config.ts",
|
|
36
|
-
"test:smoke": "CLI_SMOKE=1 jest -c jest.config.ts src/__tests__/create.smoke.test.ts"
|
|
37
|
+
"test:smoke": "CLI_SMOKE=1 jest -c jest.config.ts src/__tests__/create.smoke.test.ts",
|
|
38
|
+
"prepare:templates": "tsx scripts/prepare-templates.ts"
|
|
37
39
|
}
|
|
38
40
|
}
|
|
@@ -8,6 +8,23 @@ alwaysApply: false
|
|
|
8
8
|
All server communication goes through tRPC with the TanStack React Query
|
|
9
9
|
integration. No `fetch`, no axios, no hand-written API clients.
|
|
10
10
|
|
|
11
|
+
## Types come from the server — never hand-mirror them
|
|
12
|
+
|
|
13
|
+
Row and payload types are **inferred from the router**, not redeclared:
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
import type { inferRouterOutputs } from "@trpc/server";
|
|
17
|
+
import type { AppRouter } from "{{PACKAGE_SCOPE}}/server/types";
|
|
18
|
+
|
|
19
|
+
type PostsListOutput = inferRouterOutputs<AppRouter>["posts"]["list"];
|
|
20
|
+
export type PostRow = PostsListOutput["rows"][number];
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Shared Zod schemas (`apps/shared`) are only for runtime needs — form
|
|
24
|
+
validation, or schemas the server DTO also imports. Never write a shared
|
|
25
|
+
schema that just mirrors a server response; it will drift (see the shared
|
|
26
|
+
schema guide).
|
|
27
|
+
|
|
11
28
|
## Queries
|
|
12
29
|
|
|
13
30
|
Use `useTRPC()` and the generated `queryOptions`:
|
|
@@ -15,15 +32,38 @@ Use `useTRPC()` and the generated `queryOptions`:
|
|
|
15
32
|
```ts
|
|
16
33
|
const trpc = useTRPC();
|
|
17
34
|
const { data, isLoading, isFetching } = useQuery(
|
|
18
|
-
trpc.posts.list.queryOptions(listInput) as unknown as UseQueryOptions<
|
|
35
|
+
trpc.posts.list.queryOptions(listInput) as unknown as UseQueryOptions<PostsListOutput>
|
|
19
36
|
);
|
|
20
37
|
```
|
|
21
38
|
|
|
22
|
-
- Input and output types come from the shared Zod schemas in `apps/shared` —
|
|
23
|
-
never redeclare them locally.
|
|
24
39
|
- Search inputs that feed queries go through `useDeferredValue`, and URL-state
|
|
25
40
|
updates are wrapped in `startTransition`.
|
|
26
41
|
|
|
42
|
+
## List endpoints speak the generic query contract
|
|
43
|
+
|
|
44
|
+
Module list procedures accept `QueryInput` from
|
|
45
|
+
`@m5kdev/commons/modules/schemas/query.schema` — `page`, `limit`, `sort`,
|
|
46
|
+
`order`, `q` (global search), and `filters`. Do not invent per-module fields
|
|
47
|
+
like `search` or `status` at the top level; they will be stripped by input
|
|
48
|
+
validation and silently ignored.
|
|
49
|
+
|
|
50
|
+
```ts
|
|
51
|
+
const listInput = useMemo<QueryInput>(
|
|
52
|
+
() => ({
|
|
53
|
+
page,
|
|
54
|
+
limit: PAGE_SIZE,
|
|
55
|
+
q: deferredSearch || undefined,
|
|
56
|
+
filters:
|
|
57
|
+
status === "all"
|
|
58
|
+
? undefined
|
|
59
|
+
: [{ columnId: "status", type: "enum", method: "equals", value: status }],
|
|
60
|
+
sort: "updatedAt",
|
|
61
|
+
order: "desc",
|
|
62
|
+
}),
|
|
63
|
+
[deferredSearch, page, status]
|
|
64
|
+
);
|
|
65
|
+
```
|
|
66
|
+
|
|
27
67
|
## Mutations
|
|
28
68
|
|
|
29
69
|
Use `mutationOptions` with success/error handling declared where the mutation
|
|
@@ -41,6 +81,9 @@ const createMutation = useMutation(
|
|
|
41
81
|
);
|
|
42
82
|
```
|
|
43
83
|
|
|
84
|
+
Mutation input shapes are enforced by the router's inferred types — no
|
|
85
|
+
`satisfies SomeSchema` casts needed for simple `{ id }` payloads.
|
|
86
|
+
|
|
44
87
|
## Invalidation discipline
|
|
45
88
|
|
|
46
89
|
- Invalidate intentionally with the matching `queryFilter()` after mutations.
|
|
@@ -5,7 +5,10 @@ alwaysApply: false
|
|
|
5
5
|
|
|
6
6
|
# Module DB File Style Guide
|
|
7
7
|
|
|
8
|
-
Conventions for database table definitions in app modules.
|
|
8
|
+
Conventions for database table definitions in app modules. The table is the
|
|
9
|
+
**source of truth** for the module: the DTO (`<module>.dto.ts`) derives its
|
|
10
|
+
Zod schemas from it, and row types flow from there through the router to the
|
|
11
|
+
client.
|
|
9
12
|
|
|
10
13
|
## Conventions
|
|
11
14
|
|
|
@@ -23,6 +26,17 @@ Conventions for database table definitions in app modules.
|
|
|
23
26
|
- Composite join tables define primary keys with `primaryKey({ columns: [t.aId, t.bId] })`.
|
|
24
27
|
- Table names are plural; column names are snake_case at DB level, camelCase in code.
|
|
25
28
|
- Inline union types are acceptable for simple status columns: `.$type<"draft" | "published">()`.
|
|
29
|
+
- Include `deletedAt` when the module needs soft deletes — `queryList`
|
|
30
|
+
automatically excludes soft-deleted rows when the column exists.
|
|
31
|
+
|
|
32
|
+
## Registration and migrations
|
|
33
|
+
|
|
34
|
+
- Register every new table in `apps/server/src/schema.ts` with a plain
|
|
35
|
+
re-export: `export { posts } from "./modules/posts/posts.db";`. The schema
|
|
36
|
+
file is maintained by hand — there is no generation step.
|
|
37
|
+
- After changing tables, run `pnpm drizzle:generate` (creates a migration in
|
|
38
|
+
`drizzle/`) and `pnpm drizzle:migrate` (applies it). Never write migration
|
|
39
|
+
SQL by hand and never edit files under `drizzle/meta/`.
|
|
26
40
|
|
|
27
41
|
## File Location
|
|
28
42
|
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
---
|
|
2
|
+
globs: apps/**/server/src/modules/**/*.dto.ts
|
|
3
|
+
alwaysApply: false
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Module DTO Style Guide
|
|
7
|
+
|
|
8
|
+
The DTO is the module's API surface, **derived from the Drizzle table** —
|
|
9
|
+
database schema first. Services, repositories, and tRPC files import the DTO
|
|
10
|
+
*object* (`postSchemas`) and reference `postSchemas.input.x` /
|
|
11
|
+
`postSchemas.output.y`; they do not import hand-written types.
|
|
12
|
+
|
|
13
|
+
## The pattern
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
import { createZodSchemas } from "@m5kdev/backend/modules/base/base.dto";
|
|
17
|
+
import {
|
|
18
|
+
postCreateInputSchema,
|
|
19
|
+
postUpdateInputSchema,
|
|
20
|
+
} from "{{PACKAGE_SCOPE}}/shared/modules/posts/posts.schema";
|
|
21
|
+
import { z } from "zod";
|
|
22
|
+
import { posts } from "./posts.db";
|
|
23
|
+
|
|
24
|
+
const { output, input } = createZodSchemas(posts);
|
|
25
|
+
|
|
26
|
+
export const postSchemas = {
|
|
27
|
+
output,
|
|
28
|
+
input: {
|
|
29
|
+
...input,
|
|
30
|
+
// create/update come from shared: the same schemas validate the webapp
|
|
31
|
+
// form, so the API contract and frontend validation cannot drift
|
|
32
|
+
create: postCreateInputSchema,
|
|
33
|
+
update: postUpdateInputSchema,
|
|
34
|
+
publish: z.object({ id: z.string() }),
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## What `createZodSchemas(table)` provides
|
|
40
|
+
|
|
41
|
+
- `output.single` — the row (from `createSelectSchema`).
|
|
42
|
+
- `output.found` — `single.optional()`, for find-style queries that may
|
|
43
|
+
return nothing.
|
|
44
|
+
- `output.list` — `{ rows, total }` envelope.
|
|
45
|
+
- `output.uuid` / `output.uuids` — id acknowledgements.
|
|
46
|
+
- `input.list` — the generic `querySchema` (pagination, sorting, `q`,
|
|
47
|
+
`filters`).
|
|
48
|
+
- `input.read/readMany/delete/deleteMany` — id inputs.
|
|
49
|
+
- `input.create` / `input.update` — raw insert/update schemas from the table.
|
|
50
|
+
|
|
51
|
+
## Rules
|
|
52
|
+
|
|
53
|
+
- **Always override `input.create` and `input.update`** with narrower schemas:
|
|
54
|
+
the raw insert schema exposes server-managed columns (`id`, timestamps,
|
|
55
|
+
owner ids) that clients must never send. If the frontend validates the same
|
|
56
|
+
form, import the schema from shared instead of redefining it.
|
|
57
|
+
- Add action-specific inputs (`publish`, `archive`) as plain Zod objects here.
|
|
58
|
+
- Extend `input.list` only when a custom list contract is genuinely needed;
|
|
59
|
+
prefer the generic `q`/`filters` contract (see repository guide).
|
|
60
|
+
- One exported `<module>Schemas` object per module — no loose schema exports.
|