pastoria 1.0.11 → 1.0.13

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/src/build.ts CHANGED
@@ -1,206 +1,306 @@
1
- import tailwindcss from '@tailwindcss/vite';
2
- import react from '@vitejs/plugin-react';
3
- import {access} from 'node:fs/promises';
4
- import * as path from 'node:path';
1
+ import ParcelWatcher, {getEventsSince, writeSnapshot} from '@parcel/watcher';
2
+ import {spawn} from 'node:child_process';
3
+ import {access, readFile} from 'node:fs/promises';
4
+ import path from 'node:path';
5
+ import {IndentationText, Project} from 'ts-morph';
6
+ import {build} from 'vite';
5
7
  import {
6
- build,
7
- InlineConfig,
8
- type BuildEnvironmentOptions,
9
- type Plugin,
10
- } from 'vite';
11
- import {cjsInterop} from 'vite-plugin-cjs-interop';
12
-
13
- interface PastoriaCapabilities {
14
- hasAppRoot: boolean;
15
- hasServerHandler: boolean;
8
+ generatePastoriaArtifacts,
9
+ generatePastoriaExports,
10
+ PASTORIA_TAG_REGEX,
11
+ PastoriaMetadata,
12
+ } from './generate.js';
13
+ import {logger, logInfo} from './logger.js';
14
+ import {CLIENT_BUILD, createBuildConfig, SERVER_BUILD} from './vite_plugin.js';
15
+
16
+ enum PastoriaMakePhase {
17
+ PASTORIA_EXPORTS,
18
+ PASTORIA_ARTIFACTS,
19
+ RELAY,
20
+ GRATS,
16
21
  }
17
22
 
18
- function generateClientEntry({hasAppRoot}: PastoriaCapabilities): string {
19
- const appImport = hasAppRoot
20
- ? `import {App} from '#genfiles/router/app_root';`
21
- : '';
22
- const appValue = hasAppRoot ? 'App' : 'null';
23
+ const ALL_MAKE_PHASES = new Set([
24
+ PastoriaMakePhase.PASTORIA_EXPORTS,
25
+ PastoriaMakePhase.PASTORIA_ARTIFACTS,
26
+ PastoriaMakePhase.RELAY,
27
+ PastoriaMakePhase.GRATS,
28
+ ]);
29
+
30
+ const SNAPSHOT_PATH = '.pastoriainfo';
23
31
 
24
- return `// Generated by Pastoria.
25
- import {createRouterApp} from '#genfiles/router/router';
26
- ${appImport}
27
- import {hydrateRoot} from 'react-dom/client';
32
+ async function runCommand(command: string, args: string[]): Promise<void> {
33
+ return new Promise((resolve, reject) => {
34
+ const child = spawn(command, args, {
35
+ cwd: process.cwd(),
36
+ stdio: 'inherit', // Stream output to terminal
37
+ shell: true,
38
+ });
28
39
 
29
- async function main() {
30
- const RouterApp = await createRouterApp();
31
- hydrateRoot(document, <RouterApp App={${appValue}} />);
40
+ child.on('exit', (code) => {
41
+ if (code === 0) {
42
+ resolve();
43
+ } else {
44
+ reject(new Error(`Command failed with exit code ${code}`));
45
+ }
46
+ });
47
+
48
+ child.on('error', reject);
49
+ });
32
50
  }
33
51
 
34
- main();
35
- `;
52
+ async function runGratsCompiler(): Promise<void> {
53
+ const gratsPath = path.join(process.cwd(), 'node_modules', '.bin', 'grats');
54
+ await runCommand(gratsPath, []);
36
55
  }
37
56
 
38
- function generateServerEntry({
39
- hasAppRoot,
40
- hasServerHandler,
41
- }: PastoriaCapabilities): string {
42
- const appImport = hasAppRoot
43
- ? `import {App} from '#genfiles/router/app_root';`
44
- : '';
45
- const appValue = hasAppRoot ? 'App' : 'null';
46
-
47
- const serverHandlerImport = hasServerHandler
48
- ? `import {router as serverHandler} from '#genfiles/router/server_handler';`
49
- : '';
50
- const serverHandlerUse = hasServerHandler
51
- ? ' router.use(serverHandler)'
52
- : '';
53
-
54
- return `// Generated by Pastoria.
55
- import {JSResource} from '#genfiles/router/js_resource';
56
- import {
57
- listRoutes,
58
- router__createAppFromEntryPoint,
59
- router__loadEntryPoint,
60
- } from '#genfiles/router/router';
61
- import {getSchema} from '#genfiles/schema/schema';
62
- import {Context} from '#genfiles/router/context';
63
- ${appImport}
64
- ${serverHandlerImport}
65
- import express from 'express';
66
- import {GraphQLSchema, specifiedDirectives} from 'graphql';
67
- import {PastoriaConfig} from 'pastoria-config';
68
- import {createRouterHandler} from 'pastoria-runtime/server';
69
- import type {Manifest} from 'vite';
70
-
71
- const schemaConfig = getSchema().toConfig();
72
- const schema = new GraphQLSchema({
73
- ...schemaConfig,
74
- directives: [...specifiedDirectives, ...schemaConfig.directives],
75
- });
76
-
77
- export function createHandler(
78
- persistedQueries: Record<string, string>,
79
- config: Required<PastoriaConfig>,
80
- manifest?: Manifest,
81
- ) {
82
- const routeHandler = createRouterHandler(
83
- listRoutes(),
84
- JSResource.srcOfModuleId,
85
- router__loadEntryPoint,
86
- router__createAppFromEntryPoint,
87
- ${appValue},
88
- schema,
89
- (req) => Context.createFromRequest(req),
90
- persistedQueries,
91
- config,
92
- manifest,
57
+ async function runRelayCompiler(): Promise<void> {
58
+ const relayPath = path.join(
59
+ process.cwd(),
60
+ 'node_modules',
61
+ '.bin',
62
+ 'relay-compiler',
93
63
  );
64
+ await runCommand(relayPath, []);
65
+ }
94
66
 
95
- const router = express.Router();
96
- router.use(routeHandler);
97
- ${serverHandlerUse}
67
+ function fileMatchesPastoriaTags(filePath: string, content: string): boolean {
68
+ // Skip generated files
69
+ if (filePath.includes('__generated__')) {
70
+ return false;
71
+ }
72
+ return PASTORIA_TAG_REGEX.test(content);
73
+ }
98
74
 
99
- return router;
75
+ function fileMatchesGratsTags(filePath: string, content: string): boolean {
76
+ // Skip generated files
77
+ if (filePath.includes('__generated__')) {
78
+ return false;
79
+ }
80
+ // Match any Grats JSDoc tag
81
+ return /@gql\w+/.test(content);
100
82
  }
101
- `;
83
+
84
+ function fileMatchesRelayImports(filePath: string, content: string): boolean {
85
+ // Skip generated files
86
+ if (filePath.includes('__generated__')) {
87
+ return false;
88
+ }
89
+ return (
90
+ /import\s+.*\s+from\s+['"]react-relay['"]/.test(content) ||
91
+ /import\s+.*\s+from\s+['"]relay-runtime['"]/.test(content)
92
+ );
102
93
  }
103
94
 
104
- async function determineCapabilities(): Promise<PastoriaCapabilities> {
105
- const capabilities: PastoriaCapabilities = {
106
- hasAppRoot: false,
107
- hasServerHandler: false,
108
- };
95
+ async function requiredMakePhasesForChanges(
96
+ events: Array<{type: string; path: string}>,
97
+ ): Promise<Set<PastoriaMakePhase>> {
98
+ let makePhases = new Set<PastoriaMakePhase>();
109
99
 
110
- async function hasAppRoot() {
111
- try {
112
- await access('__generated__/router/app_root.ts');
113
- capabilities.hasAppRoot = true;
114
- } catch {}
115
- }
100
+ await Promise.all(
101
+ events.map(async (event) => {
102
+ const filePath = event.path;
116
103
 
117
- async function hasServerHandler() {
118
- try {
119
- await access('__generated__/router/server_handler.ts');
120
- capabilities.hasServerHandler = true;
121
- } catch {}
104
+ // Skip non-TypeScript/TSX files
105
+ if (!filePath.match(/\.(ts|tsx)$/)) {
106
+ return;
107
+ }
108
+
109
+ // For delete events, we can't read content, so assume it might affect all pipelines
110
+ if (event.type === 'delete') {
111
+ makePhases = ALL_MAKE_PHASES;
112
+ return;
113
+ }
114
+
115
+ // Read file content for create/update events
116
+ try {
117
+ const content = await readFile(filePath, 'utf-8');
118
+
119
+ if (fileMatchesPastoriaTags(filePath, content)) {
120
+ makePhases.add(PastoriaMakePhase.PASTORIA_EXPORTS);
121
+ makePhases.add(PastoriaMakePhase.PASTORIA_ARTIFACTS);
122
+ }
123
+
124
+ if (fileMatchesGratsTags(filePath, content)) {
125
+ makePhases.add(PastoriaMakePhase.GRATS);
126
+ makePhases.add(PastoriaMakePhase.RELAY); // Relay depends on Grats schema
127
+ }
128
+
129
+ if (fileMatchesRelayImports(filePath, content)) {
130
+ makePhases.add(PastoriaMakePhase.RELAY);
131
+ }
132
+ } catch {
133
+ // If we can't read the file, assume it might affect all pipelines
134
+ makePhases = ALL_MAKE_PHASES;
135
+ }
136
+ }),
137
+ );
138
+
139
+ return makePhases;
140
+ }
141
+
142
+ function requiredMakePhasesForArgs(steps: string[]): Set<PastoriaMakePhase> {
143
+ const validSteps = new Set(['schema', 'relay', 'router']);
144
+ const needs = new Set<PastoriaMakePhase>();
145
+
146
+ for (const step of steps) {
147
+ if (!validSteps.has(step)) {
148
+ throw new Error(
149
+ `Invalid build step: ${step}. Valid steps are: schema, relay, router`,
150
+ );
151
+ }
152
+
153
+ switch (step) {
154
+ case 'schema':
155
+ needs.add(PastoriaMakePhase.GRATS);
156
+ break;
157
+ case 'relay':
158
+ needs.add(PastoriaMakePhase.RELAY);
159
+
160
+ break;
161
+ case 'router':
162
+ needs.add(PastoriaMakePhase.PASTORIA_EXPORTS);
163
+ needs.add(PastoriaMakePhase.PASTORIA_ARTIFACTS);
164
+ break;
165
+ }
122
166
  }
123
167
 
124
- await Promise.all([hasAppRoot(), hasServerHandler()]);
125
- return capabilities;
168
+ return needs;
126
169
  }
127
170
 
128
- function pastoriaEntryPlugin(): Plugin {
129
- const clientEntryModuleId = 'virtual:pastoria-entry-client.tsx';
130
- const serverEntryModuleId = 'virtual:pastoria-entry-server.tsx';
131
-
132
- return {
133
- name: 'pastoria-entry',
134
- resolveId(id) {
135
- if (id === clientEntryModuleId) {
136
- return clientEntryModuleId; // Return without \0 prefix so React plugin can see .tsx extension
137
- } else if (id === serverEntryModuleId) {
138
- return serverEntryModuleId;
139
- }
140
- },
141
- async load(id) {
142
- const capabilities = await determineCapabilities();
143
- if (id === clientEntryModuleId) {
144
- return generateClientEntry(capabilities);
145
- } else if (id === serverEntryModuleId) {
146
- return generateServerEntry(capabilities);
147
- }
148
- },
149
- };
171
+ async function executeBuildSteps(
172
+ project: Project,
173
+ needs: Set<PastoriaMakePhase>,
174
+ ): Promise<boolean> {
175
+ let rebuiltAnything = false;
176
+ let cachedMetadata: PastoriaMetadata | undefined = undefined;
177
+
178
+ if (needs.has(PastoriaMakePhase.PASTORIA_EXPORTS)) {
179
+ logInfo('Running Pastoria exports generation...');
180
+ cachedMetadata = await generatePastoriaExports(project);
181
+ rebuiltAnything = true;
182
+ }
183
+
184
+ if (needs.has(PastoriaMakePhase.GRATS)) {
185
+ logInfo('Running Grats compiler...');
186
+ await runGratsCompiler();
187
+ rebuiltAnything = true;
188
+ }
189
+
190
+ if (needs.has(PastoriaMakePhase.RELAY)) {
191
+ logInfo('Running Relay compiler...');
192
+ await runRelayCompiler();
193
+ rebuiltAnything = true;
194
+ }
195
+
196
+ if (needs.has(PastoriaMakePhase.PASTORIA_ARTIFACTS)) {
197
+ logInfo('Running Pastoria artifacts generation...');
198
+ await generatePastoriaArtifacts(project, cachedMetadata);
199
+ rebuiltAnything = true;
200
+ }
201
+
202
+ return rebuiltAnything;
150
203
  }
151
204
 
152
- export const CLIENT_BUILD: BuildEnvironmentOptions = {
153
- outDir: 'dist/client',
154
- rollupOptions: {
155
- input: 'virtual:pastoria-entry-client.tsx',
205
+ export async function createBuild(
206
+ steps: string[],
207
+ opts: {
208
+ alwaysMake: boolean;
209
+ release: boolean;
210
+ watch?: boolean;
156
211
  },
157
- };
212
+ ) {
213
+ if (opts.watch && opts.release) {
214
+ throw new Error(
215
+ 'Cannot use --watch and --release together. Watch mode is for development only.',
216
+ );
217
+ }
158
218
 
159
- export const SERVER_BUILD: BuildEnvironmentOptions = {
160
- outDir: 'dist/server',
161
- ssr: true,
162
- rollupOptions: {
163
- input: 'virtual:pastoria-entry-server.tsx',
164
- },
165
- };
166
-
167
- export function createBuildConfig(
168
- buildEnv: BuildEnvironmentOptions,
169
- ): InlineConfig {
170
- return {
171
- appType: 'custom' as const,
172
- build: {
173
- ...buildEnv,
174
- assetsInlineLimit: 0,
175
- manifest: true,
176
- ssrManifest: true,
177
- },
178
- plugins: [
179
- pastoriaEntryPlugin(),
180
- tailwindcss(),
181
- react({
182
- babel: {
183
- plugins: [['babel-plugin-react-compiler', {}], 'relay'],
184
- },
185
- }),
186
- cjsInterop({
187
- dependencies: ['react-relay', 'react-relay/hooks', 'relay-runtime'],
188
- }),
189
- ],
190
- ssr: {
191
- noExternal: ['pastoria-runtime'],
219
+ const project = new Project({
220
+ tsConfigFilePath: path.join(process.cwd(), 'tsconfig.json'),
221
+ manipulationSettings: {
222
+ indentationText: IndentationText.TwoSpaces,
192
223
  },
193
- };
194
- }
195
-
196
- export async function createBuild() {
197
- const clientBuild = await build({
198
- ...createBuildConfig(CLIENT_BUILD),
199
- configFile: false,
200
224
  });
201
225
 
202
- const serverBuild = await build({
203
- ...createBuildConfig(SERVER_BUILD),
204
- configFile: false,
205
- });
226
+ const cwd = process.cwd();
227
+ let makePhases = new Set<PastoriaMakePhase>();
228
+
229
+ // If specific steps are provided, override automatic inference
230
+ if (steps.length > 0) {
231
+ makePhases = makePhases.union(requiredMakePhasesForArgs(steps));
232
+ } else if (opts.alwaysMake) {
233
+ makePhases = ALL_MAKE_PHASES;
234
+ }
235
+ // Use @parcel/watcher to get changes since last snapshot
236
+ else {
237
+ try {
238
+ // Check if snapshot exists - if not, do a full build
239
+ await access(SNAPSHOT_PATH);
240
+
241
+ // Get events since last snapshot
242
+ const events = await getEventsSince(cwd, SNAPSHOT_PATH);
243
+
244
+ if (events.length > 0) {
245
+ // Analyze which files changed and determine what needs to be rebuilt
246
+ makePhases = makePhases.union(
247
+ await requiredMakePhasesForChanges(events),
248
+ );
249
+ }
250
+ } catch (err) {
251
+ // No snapshot exists yet, or error reading it - do a full build
252
+ makePhases = ALL_MAKE_PHASES;
253
+ }
254
+ }
255
+
256
+ // Execute build pipeline conditionally
257
+ await executeBuildSteps(project, makePhases);
258
+
259
+ // Write snapshot for next incremental build
260
+ await writeSnapshot(cwd, SNAPSHOT_PATH);
261
+
262
+ if (opts.release) {
263
+ await build({
264
+ ...createBuildConfig(CLIENT_BUILD),
265
+ configFile: false,
266
+ });
267
+
268
+ await build({
269
+ ...createBuildConfig(SERVER_BUILD),
270
+ configFile: false,
271
+ });
272
+ }
273
+
274
+ // Start watch mode if requested
275
+ if (opts.watch) {
276
+ logInfo('Watching for changes...');
277
+
278
+ const subscription = await ParcelWatcher.subscribe(
279
+ cwd,
280
+ async (err, events) => {
281
+ if (err) {
282
+ logger.error('Watch error!', {error: err});
283
+ return;
284
+ }
285
+
286
+ // Analyze which files changed and determine what needs to be rebuilt
287
+ const rebuiltAnything = await executeBuildSteps(
288
+ project,
289
+ await requiredMakePhasesForChanges(events),
290
+ );
291
+
292
+ if (rebuiltAnything) {
293
+ // Write snapshot after successful rebuild
294
+ await writeSnapshot(cwd, SNAPSHOT_PATH);
295
+ logInfo('Rebuild complete. Watching for changes...');
296
+ }
297
+ },
298
+ );
299
+
300
+ // Keep the process running
301
+ process.on('SIGINT', async () => {
302
+ await subscription.unsubscribe();
303
+ process.exit(0);
304
+ });
305
+ }
206
306
  }
package/src/devserver.ts CHANGED
@@ -2,10 +2,11 @@ import cookieParser from 'cookie-parser';
2
2
  import dotenv from 'dotenv';
3
3
  import express from 'express';
4
4
  import {readFile} from 'node:fs/promises';
5
- import pc from 'picocolors';
6
5
  import {loadConfig, PastoriaConfig} from 'pastoria-config';
6
+ import pc from 'picocolors';
7
7
  import {createServer as createViteServer, type Manifest} from 'vite';
8
- import {CLIENT_BUILD, createBuildConfig} from './build.js';
8
+ import {logInfo} from './logger.js';
9
+ import {CLIENT_BUILD, createBuildConfig} from './vite_plugin.js';
9
10
 
10
11
  interface PersistedQueries {
11
12
  [hash: string]: string;
@@ -51,7 +52,7 @@ export async function startDevserver(opts: {port: string}) {
51
52
  if (err) {
52
53
  console.error(err);
53
54
  } else {
54
- console.log(pc.cyan(`Listening on port ${opts.port}!`));
55
+ logInfo(pc.cyan(`Listening on port ${opts.port}!`));
55
56
  }
56
57
  });
57
58
  }