ajo-kit 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.
package/src/vite.ts ADDED
@@ -0,0 +1,166 @@
1
+ import type { Plugin } from 'vite'
2
+ import { discover } from './discover'
3
+
4
+ type Pattern = RegExp | string | ((id: string) => boolean)
5
+
6
+ const match = (id: string, pattern: Pattern) =>
7
+ typeof pattern === 'function' ? pattern(id) :
8
+ typeof pattern === 'string' ? id.includes(pattern) :
9
+ pattern.test(id)
10
+
11
+ const any = (id: string, patterns: Pattern[]) =>
12
+ patterns.some(p => match(id, p))
13
+
14
+ /**
15
+ * Prevents server-only modules from being imported into client code.
16
+ * Tracks the import chain to catch transitive imports through barrel files.
17
+ */
18
+ const guard = (patterns: Pattern[]): Plugin => {
19
+
20
+ const chain = new Map<string, string>()
21
+
22
+ return {
23
+ name: 'ajo-server-only',
24
+ enforce: 'pre',
25
+ resolveId: {
26
+ order: 'pre',
27
+ async handler(source, importer) {
28
+
29
+ if (this.environment.name !== 'client') return
30
+
31
+ if (!importer) return
32
+
33
+ const resolved = await this.resolve(source, importer, { skipSelf: true })
34
+
35
+ if (!resolved) return
36
+
37
+ const id = resolved.id
38
+
39
+ // Track who imported this module
40
+ chain.set(id, importer)
41
+
42
+ // *.client.* modules are explicitly client-safe; their own imports are still checked
43
+ if (/\.client\.[jt]sx?$/.test(id)) return
44
+
45
+ // Check if this module is server-only
46
+ if (any(id, patterns)) {
47
+
48
+ // Build the import chain for error message
49
+ const trace = [id]
50
+ let current = importer
51
+
52
+ while (current && trace.length < 10) {
53
+ trace.unshift(current)
54
+ current = chain.get(current)!
55
+ }
56
+
57
+ const path = trace.map(p => p.replace(/^.*\/src\//, 'src/')).join('\n → ')
58
+
59
+ throw new Error(
60
+ `Server-only module imported into client code:\n\n ${path}\n\n` +
61
+ `Module "${id.replace(/^.*\/src\//, 'src/')}" cannot be imported by client code.`
62
+ )
63
+ }
64
+ }
65
+ }
66
+ }
67
+ }
68
+
69
+ const hmr = (pattern: RegExp): Plugin => ({
70
+ name: 'ajo-hmr',
71
+ apply: 'serve',
72
+ transform(code, id) {
73
+
74
+ if (!pattern.test(id)) return null
75
+
76
+ const HMR = `Symbol.for('ajo.hmr')`
77
+ const path = '/' + id.replace(/^.*?(src\/)/, '$1')
78
+
79
+ const tagged = code.match(/export\s+default\s+(\w+)\s*[\n;]/)
80
+ ? code.replace(/export\s+default\s+(\w+)/, `export default $1;$1[${HMR}]=${JSON.stringify(path)}`)
81
+ : code
82
+
83
+ const accept = `
84
+ if(import.meta.hot)import.meta.hot.accept(m=>{
85
+ if(m?.default)m.default[${HMR}]=${JSON.stringify(path)};
86
+ if(m)(globalThis.__MODULES__??=new Map).set(${JSON.stringify(path)},m),globalThis.__HMR__?.(${JSON.stringify(path)});
87
+ })`
88
+
89
+ return { code: tagged + accept, map: null }
90
+ }
91
+ })
92
+
93
+ /** Options for the ajo-kit Vite plugin. */
94
+ export interface Options {
95
+ guard?: Pattern[]
96
+ css?: string[]
97
+ }
98
+
99
+ /** Default file locations used by the kit CLI. */
100
+ export const defaults = {
101
+ database: './database.sqlite',
102
+ migrations: 'db/migrations',
103
+ seeds: 'db/seeds',
104
+ } as const
105
+
106
+ const guards = (found: ReturnType<typeof discover>): Pattern[] => [
107
+ /(handler|wares)\.[jt]sx?$/,
108
+ ...found.filter(p => p.serverOnly).map(p => new RegExp(`${p.name}/`)),
109
+ ]
110
+
111
+ /** Returns the Vite plugins required by an ajo-kit app. */
112
+ export function kit(options?: Options): Plugin[] {
113
+
114
+ const css = options?.css ?? []
115
+ const found = discover()
116
+
117
+ return [
118
+ {
119
+ name: 'ajo-kit',
120
+ resolveId(id) {
121
+ if (id === 'virtual:ajo/routes') return '\0virtual:ajo/routes'
122
+ if (id === 'virtual:ajo/handlers') return '\0virtual:ajo/handlers'
123
+ },
124
+ load(id) {
125
+ if (id === '\0virtual:ajo/routes') {
126
+ return "export const routes = import.meta.glob('/src/**/{layout,page}.{j,t}s{,x}')"
127
+ }
128
+ if (id === '\0virtual:ajo/handlers') {
129
+ return [
130
+ "export const handlers = import.meta.glob('/src/**/handler.{j,t}s{,x}')",
131
+ "export const wares = import.meta.glob('/src/**/wares.{j,t}s{,x}')",
132
+ ].join('\n')
133
+ }
134
+ },
135
+ transform(code, id) {
136
+ if (css.length && id.includes('ajo-kit') && id.endsWith('client.tsx')) {
137
+ return css.map(c => `import '${c}'`).join('\n') + '\n' + code
138
+ }
139
+ },
140
+ config() {
141
+ const aliases = found
142
+ .filter(p => p.alias)
143
+ .map(p => ({ find: new RegExp(`^@kit/${p.alias}(/|$)`), replacement: `${p.name}$1` }))
144
+
145
+ return {
146
+ ssr: { noExternal: [/^ajo-/] },
147
+ resolve: {
148
+ alias: [
149
+ ...aliases,
150
+ { find: /^@kit(\/|$)/, replacement: 'ajo-kit$1' },
151
+ { find: '/src/client', replacement: 'ajo-kit/client' },
152
+ ]
153
+ }
154
+ }
155
+ }
156
+ },
157
+ guard([...guards(found), ...(options?.guard ?? [])]),
158
+ hmr(/(page|layout)\.[jt]sx?$/),
159
+ ]
160
+ }
161
+
162
+ /** Vite esbuild JSX settings for Ajo components. */
163
+ export const jsx = {
164
+ jsx: 'automatic',
165
+ jsxImportSource: 'ajo',
166
+ } as const