duto 0.0.0 → 0.260808.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.
@@ -0,0 +1,209 @@
1
+ // import type { ComponentType, FunctionComponent, VNode } from 'preact'
2
+ import type { FunctionComponent, VNode } from 'preact'
3
+
4
+ export type PluginOptions = {
5
+ root?: string,
6
+ // Directory to scan for pages, relative to Vite root. Default: "./pages"
7
+ pagesDir?: string,
8
+ }
9
+
10
+ export type DepMetadata = {
11
+ path: string,
12
+ ref: string,
13
+ }
14
+
15
+ export type FileMetadata = {
16
+ path: string,
17
+ content: string,
18
+ deps: Map<string, DepMetadata>,
19
+ }
20
+
21
+ export type I18nMetadata = {
22
+ fallbackLocale: string,
23
+ locales: string[],
24
+ messages: Record<string, Record<string, string>>,
25
+ routes: Record<string, Record<string, string>>,
26
+ routeMap: Record<string, string>,
27
+ pageRoutes: { locale: string, path: string }[]
28
+ options: Record<string, string>,
29
+ string: string,
30
+ }
31
+
32
+ export type SummaryMetadata = {
33
+ type: number,
34
+ path: string,
35
+ size: number,
36
+ sizeBr: number,
37
+ sizeJS: number,
38
+ children?: Map<string, SummaryMetadata>,
39
+ }
40
+
41
+ export type Metadata = {
42
+ root: string,
43
+ dir: string,
44
+ aliases: Record<string, string>,
45
+ map: Map<string, FileMetadata>,
46
+ violations: Map<string, DepMetadata[]>,
47
+ layouts: Map<string, IRoute>, // all layouts
48
+ nf: Map<string, IRoute>, // all 404
49
+ pages: Map<string, IRoute>, // all
50
+ spages: Set<string>, // ssg pages
51
+ routes: Routes, // client-side
52
+ // routes: Map<string, IRoute>, // client-side
53
+ assets: string[],
54
+ i18n: I18nMetadata,
55
+ string: string,
56
+ summary: Map<string, SummaryMetadata>,
57
+ }
58
+
59
+ export type Page<P = {}> = FunctionComponent<VNode<P>>
60
+ // A page module as imported by Bun/Vite
61
+ export type PageModule<P = {}> = {
62
+ default: Page<P>,
63
+ // default: ComponentType<VNode<any>>,
64
+ getStaticProps?: GetStaticProps,
65
+ getProps?: GetProps,
66
+ }
67
+
68
+ /**
69
+ * getStaticProps — build-only.
70
+ * For static pages (no dynamic segments): returns a single props object.
71
+ * For dynamic pages: returns an array of { [routeParam]: value, ...props }
72
+ *
73
+ */
74
+ export type GetStaticProps =
75
+ | (() => StaticProps | Promise<StaticProps>)
76
+ | (() => StaticProps[] | Promise<StaticProps[]>)
77
+ export type StaticProps = Record<string, unknown>
78
+
79
+ /** browser-only */
80
+ // export type $duto = {
81
+ // sroutes: Set<string>,
82
+ // navigate: {},
83
+ // route: { pattern: string, params: RouteParams, path: string },
84
+ // }
85
+ export type GetProps = (params: RouteParams) => StaticProps | Promise<StaticProps>
86
+ export type RouteParams = Record<string, string | string[] | undefined>
87
+
88
+ export type Routes = IRoute[]
89
+ export type IRoute = {
90
+ content: string,
91
+ mod: PageModule,
92
+ /** True if the page exports getStaticProps */
93
+ hasGetStaticProps: boolean,
94
+ /** True if the page exports getProps */
95
+ hasGetProps: boolean,
96
+ isClientOnly: boolean,
97
+ islands: ImportUsage[], // TODO mover para cá
98
+ // islands: IslandMeta[],
99
+ // safeFilePath: string,
100
+ layouts: IRoute[],
101
+ importName: string,
102
+
103
+ ////////////////////////////
104
+
105
+ fileExt: string,
106
+ fileName: string,
107
+ /** Original file path relative to pages dir, e.g. `blog/[slug].tsx` */
108
+ filePath: string,
109
+ fullFilePath: string,
110
+ /** Wouter-compatible pattern, e.g. `/blog/:slug` */
111
+ pattern: string,
112
+ /** Params extracted from the path segments */
113
+ params: Record<string, RouteParam>,
114
+ // params: RouteParam[],
115
+ /**
116
+ * Similar to the "params" field, but with more associated metadata. For example, for `/site/[blog]/[...slug].tsx`, the segments are:
117
+ *
118
+ * 1. `{ content: 'site', dynamic: false, spread: false }`
119
+ * 2. `{ content: 'blog', dynamic: true, spread: false }`
120
+ * 3. `{ content: '...slug', dynamic: true, spread: true }`
121
+ */
122
+ // segments: RoutePart[][]; // alternativa ao "params"
123
+ is404: boolean,
124
+ /** True if this route has any dynamic segments */
125
+ isDynamic: boolean,
126
+ /** True if this route are internal */
127
+ isInternal: boolean,
128
+ }
129
+
130
+ export type RouteParam = {
131
+ name: string,
132
+ optional: boolean,
133
+ catchAll: boolean,
134
+ value?: string | string[],
135
+ }
136
+
137
+
138
+
139
+ export interface Usage {
140
+ type: 'jsx' | 'call' | 'reference'
141
+ source: string,
142
+ imported: string,
143
+ local: string,
144
+ start: number,
145
+ end: number,
146
+ text: string,
147
+ node: unknown,
148
+ parent: unknown,
149
+ }
150
+
151
+ export interface ImportUsage {
152
+ source: string,
153
+ imported: string,
154
+ local: string,
155
+ importStart: number,
156
+ importEnd: number,
157
+ importText: string,
158
+ usages: Usage[],
159
+ }
160
+
161
+ export interface IslandMeta {
162
+ /** Identificador curto e único (base64url de 7 chars) */
163
+ id: string
164
+ /** URL pública do asset JS do componente */
165
+ url: string
166
+ /** Nome do export usado */
167
+ export: string
168
+ /** Props serializadas como JSON (HTML-encoded) */
169
+ props: string
170
+ /** HTML do SSR do componente */
171
+ ssrHtml: string
172
+ }
173
+
174
+ // export interface ReplaceIslandsOptions {
175
+ // /**
176
+ // * Função que resolve o nome do componente para a URL pública do asset JS.
177
+ // * Ex: 'Counter' → '/assets/Counter.BQxoyJbC.js'
178
+ // */
179
+ // resolveUrl: (exportName: string) => string
180
+ // /**
181
+ // * Função que carrega o módulo do componente para fazer SSR.
182
+ // * Recebe o source da importação (ex: '@/components/counter2') e o nome do export.
183
+ // */
184
+ // loadComponent: (source: string, exportName: string) => Promise<unknown>
185
+ // }
186
+
187
+
188
+ // export type Island = {
189
+ // /** Unique id — written as data-island on the placeholder div */
190
+ // id: string,
191
+ // /** Component display name — used by the client to look up the component */
192
+ // name: string,
193
+ // /** Props serialised into the placeholder for client hydration */
194
+ // props: Record<string, unknown>,
195
+ // }
196
+
197
+ // export type RenderResult = {
198
+ // html: string,
199
+ // islands: Island[],
200
+ // /** true if any islands were found (= client JS needed) */
201
+ // hasIslands: boolean,
202
+ // }
203
+
204
+ // export type IslandMeta = {
205
+ // id: string,
206
+ // name: string,
207
+ // props: Record<string, unknown>,
208
+ // }
209
+
@@ -0,0 +1,5 @@
1
+ import { Envir } from 't0n'
2
+ import { dirname, join } from 'pathe'
3
+
4
+ export const _duto = join(dirname(new URL(import.meta.url).pathname), '..')
5
+ export const _root = Envir.get('npm_config_local_prefix') || Envir.get('PWD') || join(_duto, '../../../')
@@ -0,0 +1,324 @@
1
+ // REF: https://github.com/tailwindlabs/tailwindcss/blob/main/packages/%40tailwindcss-vite/src/index.ts
2
+
3
+ import { existsSync, readFileSync, rmSync } from 'node:fs'
4
+ import { join } from 'node:path'
5
+ import { defineConfig } from 'vite'
6
+
7
+ import preact from '@preact/preset-vite'
8
+ import tailwind from '@tailwindcss/vite'
9
+
10
+ import { EXT_RE } from './constants'
11
+ import { setAsset, setChunk, setEntry } from './assets'
12
+ import { renderPage } from './'
13
+ import { removeExports, resolveImport } from './ast'
14
+ import { getMetadata } from './meta'
15
+
16
+ import type { IncomingMessage, ServerResponse } from 'node:http'
17
+ import type {
18
+ // Plugin,
19
+ UserConfig, ConfigEnv, ResolvedConfig,
20
+ PreviewServer,
21
+ // ViteDevServer,
22
+ EnvironmentOptions,
23
+ PluginOption,
24
+ } from 'vite'
25
+ import type { Metadata, PluginOptions } from './types'
26
+ import { _duto, _root } from './utils'
27
+
28
+ const VIRTUAL_ID = ':duto'
29
+ const RESOLVED_ID = '\0'+ VIRTUAL_ID
30
+
31
+ export const config = (opts: UserConfig & PluginOptions) => {
32
+ if (!opts.root) opts.root = _root
33
+ if (!opts.pagesDir) opts.pagesDir = 'pages',
34
+
35
+ opts.logLevel = 'silent'
36
+ opts.plugins = [
37
+ ...(opts.plugins || []),
38
+ preact(),
39
+ tailwind(),
40
+ plugin(opts),
41
+ ]
42
+
43
+ return defineConfig(opts)
44
+ }
45
+
46
+ export default config
47
+
48
+ let metadata: Metadata
49
+ export function meta() {
50
+ return metadata
51
+ }
52
+
53
+ export function plugin(options: PluginOptions = {}): PluginOption[] {
54
+ // let server: ViteDevServer
55
+ let config: ResolvedConfig
56
+ const {
57
+ root = _root, // process.env.npm_config_local_prefix || process.env.PWD || process.cwd() || __dirname,
58
+ pagesDir = 'pages',
59
+ } = options
60
+
61
+ // let metadata: Metadata
62
+
63
+ const PAGES_RE = new RegExp(`\/${pagesDir}\/.*\.(tsx?|jsx?)$`, 'i')
64
+ const stripTarget = new Set(['getStaticProps'])
65
+
66
+ return [
67
+ {
68
+ name: 'duto:preact-environment',
69
+
70
+ configEnvironment(name, options) {
71
+ const opts: EnvironmentOptions = {optimizeDeps: {}, resolve: {}}
72
+
73
+ if (name === 'client') {
74
+ opts.optimizeDeps!.include = [
75
+ 'preact',
76
+ 'preact/jsx-runtime',
77
+ ]
78
+ }
79
+
80
+ opts.resolve = {dedupe: ['preact/compat', 'preact']}
81
+
82
+ if (name === 'client')
83
+ opts.optimizeDeps!.include!.push(
84
+ 'preact/compat',
85
+ // 'preact/test-utils',
86
+ 'preact/compat/jsx-runtime',
87
+ )
88
+
89
+ if (
90
+ !options.resolve?.noExternal &&
91
+ (name === 'ssr' || name === 'prerender')
92
+ ) {
93
+ // noExternal React entrypoints to be bundled, resolved, and aliased by Vite
94
+ opts.resolve!.noExternal = [
95
+ 'react',
96
+ 'react-dom',
97
+ 'react-dom/test-utils',
98
+ 'react/jsx-runtime',
99
+ ]
100
+ }
101
+
102
+ return opts
103
+ },
104
+
105
+ // configurePreviewServer(server: PreviewServer) {
106
+ // const distDir = join(server.config.root, 'dist')
107
+
108
+ // server.middlewares.use((req: IncomingMessage, res: ServerResponse, next: (err?: any) => void) => {
109
+ // if (req.url && !existsSync(join(distDir, req.url.split('?')[0]))) {
110
+ // const notFound = join(distDir, '404.html')
111
+
112
+ // if (existsSync(notFound)) {
113
+ // res.statusCode = 404
114
+ // res.setHeader('Content-Type', 'text/html')
115
+ // .end(readFileSync(notFound, 'utf-8'))
116
+ // return
117
+ // }
118
+ // }
119
+
120
+ // next()
121
+ // })
122
+ // },
123
+
124
+ },
125
+
126
+ {
127
+ name: 'duto:scan',
128
+ enforce: 'pre',
129
+
130
+ // configureServer(_server) {
131
+ // server = _server
132
+ // // servers.push(server)
133
+ // },
134
+
135
+ async config(conf: UserConfig, env: ConfigEnv) {
136
+ // if (!metadata)
137
+ metadata = await getMetadata(root, pagesDir)
138
+
139
+ // console.log(metadata.map)
140
+ // console.log(metadata.violations)
141
+ // console.log(Array.from(metadata.violations).filter(file => EXT_RE.test(file)))
142
+
143
+ if (conf.build) {
144
+ // conf.build.emptyOutDir = false
145
+ conf.build.copyPublicDir = false
146
+ conf.build.manifest = false
147
+ conf.build.modulePreload = { polyfill: false }
148
+
149
+ conf.build.minify = true
150
+ conf.build.cssMinify = true //////
151
+
152
+ conf.build.rolldownOptions = conf.build.rolldownOptions || {}
153
+ conf.build.rolldownOptions.preserveEntrySignatures = 'exports-only' ///
154
+ conf.build.rolldownOptions.input = [
155
+ ':duto/client/app.tsx',
156
+ // ':duto/not-found.tsx',
157
+ ...Object.keys(metadata.pages),
158
+ ...metadata.assets,
159
+ ]
160
+ // conf.build.rolldownOptions.input = ':duto/client/app.tsx'
161
+ conf.build.rolldownOptions.output = {
162
+ hoistTransitiveImports: false,
163
+ format: 'esm',
164
+ minifyInternalExports: true,
165
+
166
+ // manualChunks: {
167
+ // vendor: ['preact', '@preact/signals', 'wouter-preact'],
168
+ // },
169
+ // preserveModules: true,
170
+ preserveModulesRoot: '/',
171
+ assetFileNames: '[hash][extname]',
172
+ entryFileNames: '[hash].js',
173
+ chunkFileNames: '[hash].js',
174
+
175
+ // chunkFileNames: '[name].[hash].js',
176
+ // entryFileNames: 'assets/[name].js',
177
+ // chunkFileNames: 'assets/chunks/[name]-[hash].js',
178
+ // assetFileNames: 'assets/[name][extname]',
179
+ }
180
+ }
181
+
182
+ return conf
183
+ },
184
+
185
+ async configResolved(conf) {
186
+ config = conf
187
+ },
188
+
189
+ resolveId(id) {
190
+ if (id === VIRTUAL_ID) return RESOLVED_ID
191
+
192
+ if (id.startsWith(':root')) {
193
+ for (const f of [
194
+ 'style.css',
195
+ 'styles.css',
196
+ 'styles/index.css',
197
+ 'style/index.css',
198
+ 'tailwind.css',
199
+ 'tailwindcss.css',
200
+ 'styles/tailwind.css',
201
+ 'styles/tailwindcss.css',
202
+ ]) {
203
+ const path = join(config.root, f)
204
+ if (existsSync(path)) {
205
+ setEntry(id, path)
206
+ return path
207
+ }
208
+ }
209
+
210
+ return null
211
+ }
212
+
213
+ if (!id.startsWith(VIRTUAL_ID)) return null
214
+
215
+ if (EXT_RE.test(id) && id.includes('/')) {
216
+ const path = join(_duto, id.replace(VIRTUAL_ID, ''))
217
+ setEntry(id, path)
218
+ return path
219
+ }
220
+
221
+ if (id.endsWith('/hmr') && global.__hmr)
222
+ return join(_duto, id.replace(VIRTUAL_ID, '') + '.ts')
223
+
224
+ return id.replace(VIRTUAL_ID, RESOLVED_ID)
225
+ },
226
+
227
+ async load(id) {
228
+ if (!id.startsWith(RESOLVED_ID)) return
229
+
230
+ if (id.endsWith('/i18n'))
231
+ return metadata.i18n.string
232
+
233
+ return metadata.string
234
+ },
235
+
236
+ generateBundle(_, bundle) {
237
+ // const moduleIds = this.getModuleIds()
238
+ // for (const id of moduleIds)
239
+ // console.error(id)
240
+
241
+ for (const key in bundle) {
242
+ const file = bundle[key]
243
+ if (file.type === 'chunk' && file.isEntry) {
244
+ setChunk(file.facadeModuleId, '/'+ file.fileName)
245
+ continue
246
+ }
247
+
248
+ if (file.type === 'asset' && file?.originalFileName?.endsWith('duto/client/app.tsx'))
249
+ file.source += 'd-b,d-p,i-d,i-s,i-i{display:contents}'
250
+
251
+ if (file.type === 'asset' && file.fileName.endsWith('.css')) {
252
+ setAsset(resolveImport(file.originalFileName as string, metadata.root, metadata.aliases), '/'+ file.fileName)
253
+ continue
254
+ }
255
+
256
+ // console.error(file.fileName)
257
+ // console.error(file.type)
258
+ // console.error(file.viteMetadata)
259
+ // if (file.type !== 'chunk' || !file.isEntry) continue
260
+ }
261
+ },
262
+
263
+ },
264
+
265
+ {
266
+ name: 'duto:build',
267
+ enforce: 'pre',
268
+ apply: 'build',
269
+
270
+ async buildStart() {
271
+ // if (!server) {
272
+ // const { createServer } = await import('vite')
273
+ // server = await createServer({ server: {middlewareMode: true} })
274
+ // }
275
+
276
+
277
+ // teste de import de assets
278
+ // for (const file of metadata.assets) {
279
+ // const id = await this.resolve(file)
280
+ // if (!id) continue
281
+
282
+ // console.error(id)
283
+ // console.error(relative(metadata.root, file).replace(/\.(ts|tsx)$/, '.js'),)
284
+
285
+ // this.emitFile({
286
+ // type: 'chunk',
287
+ // id: id.id,
288
+ // implicitlyLoadedAfterOneOf: [
289
+ // ':duto/app.tsx',
290
+ // ]
291
+ // // fileName: 'assets/[name]-[hash][extname]',
292
+ // // fileName: relative(metadata.root, file).replace(/\.(ts|tsx)$/, '.js'),
293
+ // })
294
+ // }
295
+
296
+ },
297
+
298
+ async closeBundle() {
299
+ for (const [_filePath, page] of metadata.pages) {
300
+ await renderPage(metadata, config, page)
301
+ // const mod = await server.ssrLoadModule(_filePath)
302
+ }
303
+
304
+ rmSync(join(metadata.root, 'tmp'), {recursive: true, force: true})
305
+ // await server.close()
306
+ },
307
+
308
+ // TODO: precisa ser aqui mesmo pois se a pagina for importada no entry, pode incluir codigo nao usado no chunck/bundle
309
+ async transform(code, id) {
310
+ if (!PAGES_RE.test(id) || id.includes('node_modules')) return null
311
+ // console.log(this.getModuleInfo(id))
312
+
313
+ const stripped = removeExports(id, code, stripTarget)
314
+ if (!stripped || stripped === code) return null
315
+
316
+ return {
317
+ code: stripped,
318
+ map: null, // source maps omitted for brevity, add if needed
319
+ }
320
+ },
321
+
322
+ },
323
+ ]
324
+ }
@@ -0,0 +1,38 @@
1
+ import { Link as WouterLink, useLocation } from 'wouter-preact'
2
+ import type { LinkProps } from 'wouter-preact'
3
+
4
+ const isExternalUrl = (href?: string) =>
5
+ href ? href.startsWith('http')
6
+ || href.startsWith('//')
7
+ || href.startsWith('mailto:')
8
+ || href.startsWith('tel:') : false
9
+
10
+ export const Link = ({
11
+ to = '',
12
+ href = to,
13
+ ...rest
14
+ }: LinkProps) => {
15
+ const isExternal = isExternalUrl(rest.href = href)
16
+ const isSRoute = window?.$duto?.sroutes?.has(href)
17
+
18
+ if (isExternal) {
19
+ rest.target = '_blank'
20
+ rest.rel = 'noopener noreferrer'
21
+ }
22
+
23
+ if (!href || isExternal || isSRoute) {
24
+ const [pathname] = useLocation()
25
+ const baseClass = typeof rest.class === 'string' || !rest?.class
26
+ ? (rest.class || '') as string
27
+ : ''
28
+ rest.class = baseClass + (pathname === href ? ' active' : '')
29
+
30
+ !href && (delete rest.href)
31
+ return <a is='i-l' {...rest} />
32
+ }
33
+
34
+ if (typeof rest.class === 'string' || !rest?.class)
35
+ rest.className = a => (rest.class || '') + (a ? ' active' : '')
36
+
37
+ return <WouterLink is='i-l' {...rest} />
38
+ }