create-flowdular 0.1.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.
Files changed (53) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +51 -0
  3. package/dist/bin.js +526 -0
  4. package/package.json +35 -0
  5. package/template/default/.prettierrc.json +6 -0
  6. package/template/default/README.md +56 -0
  7. package/template/default/_gitignore +8 -0
  8. package/template/default/flowdular.json +14 -0
  9. package/template/default/modules/example/migrations/0001_example_core.down.sql +1 -0
  10. package/template/default/modules/example/migrations/0001_example_core.up.sql +14 -0
  11. package/template/default/modules/example/module.json +22 -0
  12. package/template/default/modules/example/package.json +27 -0
  13. package/template/default/modules/example/spec/module.yaml +43 -0
  14. package/template/default/modules/example/src/acl/permissions.ts +6 -0
  15. package/template/default/modules/example/src/api/endpoints.ts +81 -0
  16. package/template/default/modules/example/src/client/NotesView.tsrx +236 -0
  17. package/template/default/modules/example/src/client/api.ts +38 -0
  18. package/template/default/modules/example/src/client/contribution.tsrx +52 -0
  19. package/template/default/modules/example/src/client/index.ts +18 -0
  20. package/template/default/modules/example/src/client/navigation-copy.ts +9 -0
  21. package/template/default/modules/example/src/client/state.ts +14 -0
  22. package/template/default/modules/example/src/domain/types.ts +12 -0
  23. package/template/default/modules/example/src/index.ts +24 -0
  24. package/template/default/modules/example/src/platform.ts +23 -0
  25. package/template/default/modules/example/src/server/index.ts +3 -0
  26. package/template/default/modules/example/src/server/runtime.ts +84 -0
  27. package/template/default/modules/example/src/services/database-repository.ts +82 -0
  28. package/template/default/modules/example/src/services/index.ts +6 -0
  29. package/template/default/modules/example/src/services/migration.ts +35 -0
  30. package/template/default/modules/example/src/services/note-service.ts +54 -0
  31. package/template/default/modules/example/src/services/repository.ts +7 -0
  32. package/template/default/modules/example/tests/module.test.ts +67 -0
  33. package/template/default/modules/example/tests/notes.test.ts +83 -0
  34. package/template/default/modules/example/tests/support/database.ts +69 -0
  35. package/template/default/modules/example/translations/en.json +34 -0
  36. package/template/default/modules/example/translations/pl.json +34 -0
  37. package/template/default/modules/example/tsconfig.json +14 -0
  38. package/template/default/modules/example/vitest.config.ts +10 -0
  39. package/template/default/package.json +26 -0
  40. package/template/default/platform/index.html +13 -0
  41. package/template/default/platform/octane.config.ts +117 -0
  42. package/template/default/platform/package.json +27 -0
  43. package/template/default/platform/public/favicon.svg +1 -0
  44. package/template/default/platform/scripts/dev.mjs +18 -0
  45. package/template/default/platform/src/App.tsrx +80 -0
  46. package/template/default/platform/src/generated/modules.client.ts +15 -0
  47. package/template/default/platform/src/generated/modules.server.ts +31 -0
  48. package/template/default/platform/src/server/database.ts +25 -0
  49. package/template/default/platform/src/styles.css +39 -0
  50. package/template/default/platform/tsconfig.json +15 -0
  51. package/template/default/platform/vite.config.ts +16 -0
  52. package/template/default/pnpm-workspace.yaml +6 -0
  53. package/template/default/tsconfig.base.json +18 -0
@@ -0,0 +1,117 @@
1
+ import {
2
+ assertRouteConflicts,
3
+ createModuleWebRoutes,
4
+ } from '@flowdular/sdk/server';
5
+ import { resolve } from 'node:path';
6
+ import { defineConfig, RenderRoute } from '@octanejs/vite-plugin';
7
+ import {
8
+ authRuntimeOptionsFromEnvironment,
9
+ createAuthRoutes,
10
+ principalFromContext,
11
+ createAuthRuntime,
12
+ createPlatformAgentRegistry,
13
+ createPlatformCapabilityRegistry,
14
+ createPlatformToolRegistry,
15
+ } from '@flowdular/sdk/modules/auth/server';
16
+ import {
17
+ composeModuleServer,
18
+ moduleWebMounts,
19
+ } from './src/generated/modules.server.ts';
20
+ import {
21
+ createPlatformDatabaseProvider,
22
+ databaseProviderConfigFromEnvironment,
23
+ } from './src/server/database.ts';
24
+
25
+ function checkedRoutes<T extends Parameters<typeof assertRouteConflicts>[0]>(
26
+ routes: T,
27
+ ): T {
28
+ assertRouteConflicts(routes);
29
+ return routes;
30
+ }
31
+
32
+ const SHELL = ['App', '/src/App.tsrx'] as const;
33
+ const workspaceRoot = resolve(import.meta.dirname, '..');
34
+
35
+ const databases = createPlatformDatabaseProvider(
36
+ databaseProviderConfigFromEnvironment(process.env, workspaceRoot),
37
+ );
38
+ const authRuntime = createAuthRuntime({
39
+ ...authRuntimeOptionsFromEnvironment(process.env, workspaceRoot),
40
+ databases,
41
+ });
42
+
43
+ /* Module APIs come from the generated composition. Enable or disable modules
44
+ with "pnpm flowdular module enable <id> --apply"; never wire them here by hand. */
45
+ const settings = authRuntime.moduleSettings;
46
+ const agentDefinitions = createPlatformAgentRegistry();
47
+ const moduleCompositions = composeModuleServer({
48
+ environment: process.env,
49
+ workspaceRoot,
50
+ auth: authRuntime,
51
+ settings,
52
+ agentTools: createPlatformToolRegistry(),
53
+ agentDefinitions,
54
+ capabilities: createPlatformCapabilityRegistry(),
55
+ databases,
56
+ });
57
+ for (const composition of moduleCompositions) {
58
+ if (composition.settings) settings.declare(composition.settings);
59
+ }
60
+ agentDefinitions.seal();
61
+
62
+ /* check() proves the runtime role holds neither SUPERUSER nor BYPASSRLS before
63
+ any module reads a row. */
64
+ await databases.check();
65
+ for (const composition of moduleCompositions) await composition.prepare?.();
66
+ for (const composition of moduleCompositions) composition.start?.();
67
+
68
+ let stopping = false;
69
+ const shutdown = async () => {
70
+ if (stopping) return;
71
+ stopping = true;
72
+ for (const composition of moduleCompositions) {
73
+ await composition.stop?.();
74
+ await composition.dispose?.();
75
+ }
76
+ await authRuntime.dispose();
77
+ await databases.dispose();
78
+ };
79
+ process.once('SIGINT', () => void shutdown());
80
+ process.once('SIGTERM', () => void shutdown());
81
+
82
+ export default defineConfig({
83
+ middlewares: [authRuntime.middleware],
84
+ router: {
85
+ routes: checkedRoutes([
86
+ new RenderRoute({ path: '/', entry: SHELL }),
87
+ new RenderRoute({ path: '/auth', entry: SHELL }),
88
+ new RenderRoute({ path: '/auth/login', entry: SHELL }),
89
+ new RenderRoute({ path: '/auth/register', entry: SHELL }),
90
+ new RenderRoute({ path: '/auth/forgot-password', entry: SHELL }),
91
+ new RenderRoute({ path: '/auth/reset-password', entry: SHELL }),
92
+ new RenderRoute({ path: '/auth/accept-invitation', entry: SHELL }),
93
+ new RenderRoute({ path: '/auth/mfa', entry: SHELL }),
94
+ new RenderRoute({ path: '/app', entry: SHELL }),
95
+ new RenderRoute({ path: '/app/:workspace', entry: SHELL }),
96
+ new RenderRoute({ path: '/app/:workspace/:view', entry: SHELL }),
97
+ ...createAuthRoutes(authRuntime),
98
+ ...moduleCompositions.flatMap((composition) => composition.routes),
99
+ ...createModuleWebRoutes({
100
+ modules: moduleCompositions,
101
+ mounts: moduleWebMounts,
102
+ resolveIdentity: (context) => {
103
+ const principal = principalFromContext(context);
104
+ return principal
105
+ ? {
106
+ subjectId: principal.accountId,
107
+ tenantId: principal.tenantId,
108
+ permissions: new Set(principal.scopes),
109
+ }
110
+ : null;
111
+ },
112
+ }),
113
+ new RenderRoute({ path: '/:workspace', entry: SHELL }),
114
+ new RenderRoute({ path: '/:workspace/:view', entry: SHELL }),
115
+ ]),
116
+ },
117
+ });
@@ -0,0 +1,27 @@
1
+ {
2
+ "name": "@app/platform",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "node scripts/dev.mjs",
8
+ "preview": "octane-preview --host 127.0.0.1 --port 4310 --strictPort",
9
+ "typecheck": "tsrx-tsc --noEmit -p tsconfig.json"
10
+ },
11
+ "dependencies": {
12
+ "@app/module-example": "workspace:*",
13
+ "@octanejs/seo": "0.0.38",
14
+ "@octanejs/vite-plugin": "0.1.51",
15
+ "octane": "0.1.51",
16
+ "pg": "8.23.0",
17
+ "@flowdular/sdk": "0.1.0"
18
+ },
19
+ "devDependencies": {
20
+ "@octanejs/app-core": "0.0.47",
21
+ "@tsrx/typescript-plugin": "0.3.120",
22
+ "@types/node": "24.13.3",
23
+ "@types/pg": "8.23.1",
24
+ "typescript": "5.9.3",
25
+ "vite": "8.2.2"
26
+ }
27
+ }
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><rect width="24" height="24" rx="5.4" fill="#2557D6"/><g transform="translate(12 12) scale(0.64) translate(-12 -12)"><circle cx="7.9" cy="3.4" r="1.7" fill="#fff"/><rect x="6.2" y="3.4" width="3.4" height="1.6" fill="#fff"/><rect x="6.2" y="10.8" width="3.4" height="9.8" fill="#fff"/><circle cx="7.9" cy="20.6" r="1.7" fill="#fff"/><circle cx="16.1" cy="3.4" r="1.7" fill="#fff"/><rect x="14.4" y="3.4" width="3.4" height="9.8" fill="#fff"/><rect x="14.4" y="19" width="3.4" height="1.6" fill="#fff"/><circle cx="16.1" cy="20.6" r="1.7" fill="#fff"/><circle cx="3.4" cy="7.9" r="1.7" fill="#fff"/><rect x="3.4" y="6.2" width="9.8" height="3.4" fill="#fff"/><rect x="19" y="6.2" width="1.6" height="3.4" fill="#fff"/><circle cx="20.6" cy="7.9" r="1.7" fill="#fff"/><circle cx="3.4" cy="16.1" r="1.7" fill="#fff"/><rect x="3.4" y="14.4" width="1.6" height="3.4" fill="#fff"/><rect x="10.8" y="14.4" width="9.8" height="3.4" fill="#fff"/><circle cx="20.6" cy="16.1" r="1.7" fill="#fff"/></g></svg>
@@ -0,0 +1,18 @@
1
+ import { resolve } from 'node:path';
2
+ import { fileURLToPath } from 'node:url';
3
+ import { createServer } from 'vite';
4
+
5
+ const appRoot = resolve(fileURLToPath(new URL('..', import.meta.url)));
6
+
7
+ const server = await createServer({
8
+ root: appRoot,
9
+ configFile: resolve(appRoot, 'vite.config.ts'),
10
+ });
11
+ await server.listen();
12
+ server.printUrls();
13
+
14
+ /* Closing without exiting lets octane.config.ts release the database on the
15
+ same signal; the process ends once both have drained. */
16
+ const stop = () => void server.close();
17
+ process.once('SIGINT', stop);
18
+ process.once('SIGTERM', stop);
@@ -0,0 +1,80 @@
1
+ import { configureApplicationRouting } from '@flowdular/sdk/client/routing';
2
+ import {
3
+ ApplicationShell,
4
+ activeLocale,
5
+ shellLocationFromUrl,
6
+ t,
7
+ } from '@flowdular/sdk/client';
8
+ import {
9
+ AuthenticationCore,
10
+ authScreenFromUrl,
11
+ } from '@flowdular/sdk/modules/auth/client';
12
+ import type { AuthPrincipal } from '@flowdular/sdk/modules/auth';
13
+ import { Head } from '@octanejs/seo';
14
+ import type { RenderRouteProps } from '@octanejs/vite-plugin';
15
+ import { createModuleClientContributions, applicationBasePath } from './generated/modules.client.ts';
16
+ import './styles.css';
17
+
18
+ configureApplicationRouting(applicationBasePath);
19
+
20
+ interface AuthenticatedAppProps {
21
+ initialUrl: string;
22
+ principal: AuthPrincipal;
23
+ csrfToken: string;
24
+ signOut: () => Promise<void>;
25
+ switchTenant: (tenantId: string) => Promise<void>;
26
+ }
27
+
28
+ function AuthenticatedApp(props: AuthenticatedAppProps) @{
29
+ const canAccessWorkspace = props.principal.scopes.includes(
30
+ 'system.workspace.access',
31
+ );
32
+ <div>
33
+ @if (canAccessWorkspace) {
34
+ <ApplicationShell
35
+ initialView={shellLocationFromUrl(
36
+ props.initialUrl,
37
+ props.principal.tenants.map((tenant) => tenant.slug),
38
+ ).view}
39
+ contributions={createModuleClientContributions({
40
+ csrfToken: props.csrfToken,
41
+ scopes: props.principal.scopes,
42
+ locale: activeLocale(),
43
+ t,
44
+ })}
45
+ identity={props.principal}
46
+ onSignOut={props.signOut}
47
+ onSwitchTenant={props.switchTenant}
48
+ />
49
+ } @else {
50
+ <main class="access-denied">
51
+ <span>403</span>
52
+ <h1>{t('shell.accessDenied.title')}</h1>
53
+ <p>{t('shell.accessDenied.description')}</p>
54
+ <button
55
+ class="ui-btn ui-btn--primary"
56
+ type="button"
57
+ onClick={() => void props.signOut()}
58
+ >{t('shell.account.signOut')}</button>
59
+ </main>
60
+ }
61
+ </div>
62
+ }
63
+
64
+ export function App(props?: RenderRouteProps) @{
65
+ const initialUrl =
66
+ props?.url ?? (typeof window === 'undefined' ? '/' : window.location.href);
67
+ <Head>
68
+ <AuthenticationCore
69
+ initialScreen={authScreenFromUrl(initialUrl)}
70
+ renderAuthenticated={(principal, controls) =>
71
+ <AuthenticatedApp
72
+ initialUrl={initialUrl}
73
+ principal={principal}
74
+ csrfToken={controls.csrfToken}
75
+ signOut={controls.signOut}
76
+ switchTenant={controls.switchTenant}
77
+ />}
78
+ />
79
+ </Head>
80
+ }
@@ -0,0 +1,15 @@
1
+ /* GENERATED by "flowdular module sync". Do not edit by hand; run
2
+ "pnpm flowdular module sync --apply" after enabling or disabling modules. */
3
+ import type {
4
+ ModuleClientContext,
5
+ ModuleClientContribution,
6
+ } from '@flowdular/sdk/client';
7
+ import { createClientContribution as system_core } from '@flowdular/sdk/modules/system/client';
8
+ import { createClientContribution as auth_core } from '@flowdular/sdk/modules/auth/client';
9
+ import { createClientContribution as example_core } from '@app/module-example/client';
10
+
11
+ export function createModuleClientContributions(
12
+ context: ModuleClientContext,
13
+ ): readonly ModuleClientContribution[] {
14
+ return [system_core(context), auth_core(context), example_core(context)];
15
+ }
@@ -0,0 +1,31 @@
1
+ /* GENERATED by "flowdular module sync". Do not edit by hand; run
2
+ "pnpm flowdular module sync --apply" after enabling or disabling modules. */
3
+ import type {
4
+ PlatformServerComposition,
5
+ PlatformServerContext,
6
+ } from '@flowdular/sdk/modules/auth/server';
7
+ import { createServerComposition as system_core } from '@flowdular/sdk/modules/system/platform';
8
+ import { createServerComposition as example_core } from '@app/module-example/platform';
9
+
10
+ export function composeModuleServer(
11
+ context: PlatformServerContext,
12
+ ): readonly PlatformServerComposition[] {
13
+ return [
14
+ {
15
+ ...system_core({
16
+ ...context,
17
+ agentDefinitions: context.agentDefinitions.forModule('system.core'),
18
+ }),
19
+ moduleId: 'system.core',
20
+ },
21
+ {
22
+ ...example_core({
23
+ ...context,
24
+ agentDefinitions: context.agentDefinitions.forModule('example.core'),
25
+ }),
26
+ moduleId: 'example.core',
27
+ },
28
+ ];
29
+ }
30
+
31
+ export const moduleWebMounts = [] as const;
@@ -0,0 +1,25 @@
1
+ import {
2
+ createDatabaseProvider,
3
+ databaseProviderConfigFromEnvironment,
4
+ type ConfiguredDatabaseProvider,
5
+ type DatabaseProviderConfig,
6
+ type DatabaseProviderFactories,
7
+ } from '@flowdular/sdk/database';
8
+ import { createPgliteCluster } from '@flowdular/sdk/database-pglite';
9
+ import { Pool } from 'pg';
10
+
11
+ export { databaseProviderConfigFromEnvironment };
12
+
13
+ /* The deployable owns both PostgreSQL drivers: node-postgres for a deployment
14
+ and the embedded build for a local run. @flowdular/sdk/database stays driver free,
15
+ so every caller supplies them the same way. */
16
+ export function createPlatformDatabaseProvider(
17
+ config: DatabaseProviderConfig,
18
+ factories: DatabaseProviderFactories = {},
19
+ ): ConfiguredDatabaseProvider {
20
+ return createDatabaseProvider(config, {
21
+ postgresPool: (options) => new Pool(options),
22
+ pgliteCluster: (options) => createPgliteCluster(options),
23
+ ...factories,
24
+ });
25
+ }
@@ -0,0 +1,39 @@
1
+ /* Platform-specific styles. Tokens, base styles, and primitives come from
2
+ @flowdular/sdk/ui; the shell layout comes from @flowdular/sdk/client. */
3
+
4
+ .access-denied {
5
+ display: grid;
6
+ min-height: 100vh;
7
+ place-items: center;
8
+ align-content: center;
9
+ gap: 12px;
10
+ padding: 32px;
11
+ text-align: center;
12
+ background: var(--bg);
13
+ }
14
+
15
+ .access-denied > span {
16
+ font-family: var(--font-mono);
17
+ font-size: var(--text-sm);
18
+ font-weight: 500;
19
+ color: var(--copper-600);
20
+ letter-spacing: 0.08em;
21
+ }
22
+
23
+ .access-denied h1 {
24
+ margin: 0;
25
+ font-size: var(--text-2xl);
26
+ font-weight: 600;
27
+ letter-spacing: -0.01em;
28
+ }
29
+
30
+ .access-denied p {
31
+ max-width: 52ch;
32
+ margin: 0;
33
+ font-size: var(--text-md);
34
+ color: var(--ink-3);
35
+ }
36
+
37
+ .access-denied button {
38
+ margin-top: 10px;
39
+ }
@@ -0,0 +1,15 @@
1
+ {
2
+ "extends": "../tsconfig.base.json",
3
+ "compilerOptions": {
4
+ "target": "ESNext",
5
+ "module": "ESNext",
6
+ "moduleResolution": "Bundler",
7
+ "allowImportingTsExtensions": true,
8
+ "isolatedModules": true,
9
+ "jsx": "react-jsx",
10
+ "jsxImportSource": "octane",
11
+ "types": ["node"],
12
+ "plugins": [{ "name": "@tsrx/typescript-plugin" }]
13
+ },
14
+ "include": ["src/**/*", "vite.config.ts", "octane.config.ts"]
15
+ }
@@ -0,0 +1,16 @@
1
+ import { octane } from '@octanejs/vite-plugin';
2
+ import { defineConfig } from 'vite';
3
+
4
+ export default defineConfig({
5
+ plugins: [octane()],
6
+ resolve: {
7
+ extensions: ['.tsrx', '.ts', '.tsx', '.mjs', '.js', '.jsx', '.json'],
8
+ },
9
+ build: { target: 'esnext' },
10
+ server: {
11
+ port: 4310,
12
+ strictPort: true,
13
+ hmr: true,
14
+ watch: { ignored: ['**/.flowdular/data/**'] },
15
+ },
16
+ });
@@ -0,0 +1,6 @@
1
+ packages:
2
+ - platform
3
+ - modules/*
4
+
5
+ allowBuilds:
6
+ esbuild: true
@@ -0,0 +1,18 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "NodeNext",
5
+ "moduleResolution": "NodeNext",
6
+ "lib": ["ES2023", "DOM", "DOM.Iterable"],
7
+ "strict": true,
8
+ "noEmit": true,
9
+ "skipLibCheck": true,
10
+ "verbatimModuleSyntax": true,
11
+ "resolveJsonModule": true,
12
+ "allowSyntheticDefaultImports": true,
13
+ "esModuleInterop": true,
14
+ "forceConsistentCasingInFileNames": true,
15
+ "noUncheckedIndexedAccess": true,
16
+ "exactOptionalPropertyTypes": true
17
+ }
18
+ }