orga-build 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/LICENSE.org ADDED
@@ -0,0 +1,22 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2015 gatsbyjs
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
22
+
package/cli.js ADDED
@@ -0,0 +1,31 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { argv } from 'node:process'
4
+ import { parseArgs } from 'node:util'
5
+ import { watch } from './lib/watch.js'
6
+ import { loadConfig, clean } from './lib/build.js'
7
+ import { build } from './lib/esbuild/index.js'
8
+ import { serve } from './lib/serve.js'
9
+
10
+ const { values, positionals } = parseArgs({
11
+ args: argv.slice(2),
12
+ options: {
13
+ watch: { type: 'boolean', short: 'w' },
14
+ outDir: { type: 'string', short: 'o', default: 'out' }
15
+ },
16
+ tokens: true,
17
+ allowPositionals: true
18
+ })
19
+
20
+ const config = await loadConfig()
21
+
22
+ await build(config)
23
+
24
+ if (positionals.includes('dev')) {
25
+ serve(values.outDir)
26
+ watch('.', new RegExp(`^${config.outDir}`), async () => {
27
+ console.log('rebuilding')
28
+ await clean(config.outDir)
29
+ await build(config)
30
+ })
31
+ }
package/index.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ export { build } from "./lib/build.js";
2
+ //# sourceMappingURL=index.d.ts.map
package/index.d.ts.map ADDED
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["index.js"],"names":[],"mappings":""}
package/index.js ADDED
@@ -0,0 +1 @@
1
+ export { build } from './lib/build.js'
package/lib/build.d.ts ADDED
@@ -0,0 +1,61 @@
1
+ /**
2
+ * @param {typeof defaultConfig} options
3
+ */
4
+ export function build({ outDir, preBuild, postBuild }: typeof defaultConfig): Promise<void>;
5
+ /**
6
+ * @param {import("fs").PathLike} dir
7
+ */
8
+ export function clean(dir: import("fs").PathLike): Promise<void>;
9
+ /**
10
+ * @returns {Promise<typeof defaultConfig>}
11
+ */
12
+ export function loadConfig(): Promise<typeof defaultConfig>;
13
+ export type Page = {
14
+ Content: import("@orgajs/orgx").OrgContent;
15
+ /**
16
+ * - The metadata from the org file export, will be passed to the Layout component
17
+ */
18
+ metadata: Record<string, any>;
19
+ /**
20
+ * - The absolute path to the org file
21
+ */
22
+ src: string;
23
+ /**
24
+ * - The slug for the page
25
+ */
26
+ slug: string;
27
+ };
28
+ export type Layout = import("react").ComponentType<any>;
29
+ export type Pattern = string | RegExp;
30
+ export type BuildContext = {
31
+ /**
32
+ * - The components from the org file
33
+ */
34
+ components?: import("@orgajs/orgx").OrgComponents;
35
+ /**
36
+ * - The layout component
37
+ */
38
+ Layout?: Layout | undefined;
39
+ /**
40
+ * - The ignore pattern
41
+ */
42
+ ignore?: Pattern | Pattern[];
43
+ /**
44
+ * - The build function
45
+ */
46
+ build: (page: Page & {
47
+ Layout?: Layout | undefined;
48
+ components: Record<string, any>;
49
+ }) => Promise<void>;
50
+ /**
51
+ * - The build function
52
+ */
53
+ buildHref: (filePath: string, metadata: Record<string, any>) => string;
54
+ };
55
+ declare namespace defaultConfig {
56
+ let outDir: string;
57
+ let preBuild: string[];
58
+ let postBuild: string[];
59
+ }
60
+ export {};
61
+ //# sourceMappingURL=build.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"build.d.ts","sourceRoot":"","sources":["build.js"],"names":[],"mappings":"AAwJA;;GAEG;AACH,uDAFW,OAAO,aAAa,iBAwC9B;AAwBD;;GAEG;AACH,2BAFW,OAAO,IAAI,EAAE,QAAQ,iBAI/B;AAED;;GAEG;AACH,8BAFa,OAAO,CAAC,OAAO,aAAa,CAAC,CAKzC;;aAzMa,OAAO,cAAc,EAAE,UAAU;;;;cACjC,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC;;;;SACnB,MAAM;;;;UACN,MAAM;;qBAIP,OAAO,OAAO,EAAE,aAAa,CAAC,GAAG,CAAC;sBAIlC,MAAM,GAAG,MAAM;;;;;iBAKd,OAAO,cAAc,EAAE,aAAa;;;;aACpC,MAAM,GAAG,SAAS;;;;aAClB,OAAO,GAAG,OAAO,EAAE;;;;WACnB,CAAC,IAAI,EAAE,IAAI,GAAG;QAAE,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;QAAC,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;KAAE,KAAK,OAAO,CAAC,IAAI,CAAC;;;;eAChG,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,KAAK,MAAM;;;;kBA5B7D,MAAM,EAAE;mBAER,MAAM,EAAE"}
package/lib/build.js ADDED
@@ -0,0 +1,231 @@
1
+ import { globby } from 'globby'
2
+ import fs from 'node:fs/promises'
3
+ import { register } from 'node:module'
4
+ import path from 'path'
5
+ import { createElement } from 'react'
6
+ import { renderToString } from 'react-dom/server'
7
+ import assert from 'node:assert'
8
+ import { match } from './util.js'
9
+ import { evaluate, build as _build } from './esbuild.js'
10
+ import { $, DefaultLayout } from './util.js'
11
+
12
+ const USE_NODE = false
13
+
14
+ if (USE_NODE) {
15
+ register('./jsx-loader.js', import.meta.url)
16
+ register('./orga-loader.js', import.meta.url)
17
+ register('./raw-loader.js', import.meta.url)
18
+ }
19
+
20
+ const defaultConfig = {
21
+ outDir: 'out',
22
+ /** @type {string[]} */
23
+ preBuild: [],
24
+ /** @type {string[]} */
25
+ postBuild: []
26
+ }
27
+
28
+ /**
29
+ * @typedef {Object} Page
30
+ * @property {import('@orgajs/orgx').OrgContent} Content
31
+ * @property {Record<string, any>} metadata - The metadata from the org file export, will be passed to the Layout component
32
+ * @property {string} src - The absolute path to the org file
33
+ * @property {string} slug - The slug for the page
34
+ */
35
+
36
+ /**
37
+ * @typedef {import('react').ComponentType<any>} Layout
38
+ */
39
+
40
+ /**
41
+ * @typedef {string | RegExp} Pattern
42
+ */
43
+
44
+ /**
45
+ * @typedef {Object} BuildContext
46
+ * @property {import('@orgajs/orgx').OrgComponents} [components] - The components from the org file
47
+ * @property {Layout | undefined} [Layout] - The layout component
48
+ * @property {Pattern | Pattern[]} [ignore] - The ignore pattern
49
+ * @property {(page: Page & { Layout?: Layout | undefined, components: Record<string, any> }) => Promise<void>} build - The build function
50
+ * @property {(filePath: string, metadata: Record<string, any>) => string} buildHref - The build function
51
+ */
52
+
53
+ /**
54
+ * Recursively processes a directory to build pages from .org files
55
+ * @param {string} dirPath - The directory path to process
56
+ * @param {BuildContext} context - Build context containing components, layout, and build function
57
+ * @returns {Promise<void>}
58
+ */
59
+ async function iter(dirPath, context) {
60
+ /** @type {Page[]} */
61
+ const pages = []
62
+ const subdirs = []
63
+
64
+ let components = { ...context.components }
65
+ let Layout = context.Layout
66
+ let ignore = context.ignore || /node_modules/
67
+ if (!Array.isArray(ignore)) {
68
+ ignore = [ignore]
69
+ }
70
+
71
+ const files = await fs.readdir(dirPath)
72
+
73
+ for (const file of files) {
74
+ if (match(file, ...ignore)) {
75
+ continue
76
+ }
77
+ const filePath = path.join(dirPath, file)
78
+ const stat = await fs.stat(filePath)
79
+
80
+ if (stat.isDirectory()) {
81
+ subdirs.push(filePath)
82
+ continue
83
+ }
84
+
85
+ if (match(file, /(.|_)layout.(j|t)sx/)) {
86
+ const InnerLayout = (await _import(filePath)).default
87
+ if (!InnerLayout) continue
88
+ if (Layout !== undefined) {
89
+ const OuterLayout = Layout
90
+ Layout = function Layout(/** @type {any} */ props) {
91
+ return createElement(
92
+ OuterLayout,
93
+ props,
94
+ createElement(InnerLayout, props)
95
+ )
96
+ }
97
+ } else {
98
+ Layout = InnerLayout
99
+ }
100
+ continue
101
+ }
102
+ if (match(file, /(.|_)components.(j|t)sx$/)) {
103
+ const localComponents = await _import(filePath)
104
+ if (localComponents) {
105
+ components = { ...components, ...localComponents }
106
+ }
107
+ continue
108
+ }
109
+
110
+ if (file.startsWith('.')) continue
111
+
112
+ // write regex to match .org and .tsx, .jsx files, javascript code only
113
+
114
+ if (match(file, /\.(org)$/, /\.(j|t)sx$/)) {
115
+ const module = await _import(filePath)
116
+ const {
117
+ default: /** @type import('@orgajs/orgx').OrgContent */ Content,
118
+ ...metadata
119
+ } = module
120
+ pages.push({
121
+ Content,
122
+ metadata,
123
+ slug: context.buildHref(filePath, metadata),
124
+ src: filePath
125
+ })
126
+ }
127
+ }
128
+
129
+ await Promise.all(
130
+ pages.map((page) =>
131
+ context.build({
132
+ ...page,
133
+ metadata: {
134
+ ...page.metadata,
135
+ pages: pages.map((p) => ({ ...p.metadata, slug: p.slug }))
136
+ },
137
+ Layout: Layout,
138
+ components
139
+ })
140
+ )
141
+ )
142
+
143
+ // TODO: parallelize
144
+ for (const subdir of subdirs) {
145
+ await iter(subdir, {
146
+ ...context,
147
+ Layout,
148
+ components
149
+ })
150
+ }
151
+ }
152
+
153
+ /**
154
+ * @param {typeof defaultConfig} options
155
+ */
156
+ export async function build({ outDir, preBuild, postBuild }) {
157
+ for (const cmd of preBuild) {
158
+ console.log(`Running pre-build command: ${cmd}`)
159
+ await $(cmd)
160
+ }
161
+ const start = performance.now()
162
+ const cwd = process.cwd()
163
+ const outFullPath = path.join(cwd, outDir)
164
+ console.log(`Building to ${outFullPath}`)
165
+
166
+ await iter(cwd, {
167
+ buildHref: (filePath) => {
168
+ return `/${path.relative(cwd, filePath).replace(/\.\w+$/, '.html')}`
169
+ },
170
+ ignore: [/node_modules/, 'out'],
171
+ build: async ({ Layout, Content, metadata, src, components }) => {
172
+ assert(Content, 'Content component is required')
173
+ const e = createElement(
174
+ Layout ?? DefaultLayout,
175
+ metadata,
176
+ createElement(Content, { components })
177
+ )
178
+ const code = renderToString(e)
179
+ const filePath = path.relative(cwd, src).replace(/\.\w+$/, '.html')
180
+ const outPath = path.resolve(outFullPath, filePath)
181
+ await fs.mkdir(path.dirname(outPath), { recursive: true })
182
+ const filesize = new Intl.NumberFormat().format(code.length)
183
+ console.log(`${filePath} (${filesize} bytes)`)
184
+ await fs.writeFile(outPath, code, { encoding: 'utf-8', flush: true })
185
+ }
186
+ })
187
+ const end = performance.now()
188
+ console.log(`Built in ${(end - start).toFixed(2)}ms`)
189
+
190
+ for (const cmd of postBuild) {
191
+ console.log(`Running post-build command: ${cmd}`)
192
+ await $(cmd)
193
+ }
194
+ }
195
+
196
+ /**
197
+ * @param {string[]} files
198
+ */
199
+ async function _import(...files) {
200
+ const found = await globby(files, {
201
+ cwd: process.cwd(),
202
+ onlyFiles: true
203
+ })
204
+ if (found.length === 0) {
205
+ return null
206
+ }
207
+
208
+ const file = found[0]
209
+ const fullPath = path.isAbsolute(file) ? file : path.join(process.cwd(), file)
210
+ const { mtime } = await fs.stat(fullPath)
211
+ if (USE_NODE) {
212
+ return await import(`${fullPath}?version=${mtime.getTime()}`)
213
+ } else {
214
+ return await evaluate(fullPath)
215
+ }
216
+ }
217
+
218
+ /**
219
+ * @param {import("fs").PathLike} dir
220
+ */
221
+ export async function clean(dir) {
222
+ await fs.rm(dir, { recursive: true })
223
+ }
224
+
225
+ /**
226
+ * @returns {Promise<typeof defaultConfig>}
227
+ */
228
+ export async function loadConfig() {
229
+ const config = await _import('orga.config.(j|t)s')
230
+ return { ...defaultConfig, ...config }
231
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * @typedef {Object} Options
3
+ * @property {string} [outDir]
4
+ * @property {string[]} [preBuild]
5
+ * @property {string[]} [postBuild]
6
+ */
7
+ /**
8
+ * @param {Options} options
9
+ */
10
+ export function build({ outDir, preBuild, postBuild }: Options): Promise<void>;
11
+ export type Options = {
12
+ outDir?: string;
13
+ preBuild?: string[];
14
+ postBuild?: string[];
15
+ };
16
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["index.js"],"names":[],"mappings":"AAUA;;;;;GAKG;AAEH;;GAEG;AACH,uDAFW,OAAO,iBAuKjB;;aA7Ka,MAAM;eACN,MAAM,EAAE;gBACR,MAAM,EAAE"}
@@ -0,0 +1,217 @@
1
+ import fs from 'node:fs/promises'
2
+ import * as esbuild from 'esbuild'
3
+ import esbuildOrga from '@orgajs/esbuild'
4
+ import path from 'node:path'
5
+ import { createElement } from 'react'
6
+ import { renderToString } from 'react-dom/server'
7
+ import assert from 'node:assert'
8
+ import { DefaultLayout, $, match } from '../util.js'
9
+ import rawLoader from './raw-loader.js'
10
+
11
+ /**
12
+ * @typedef {Object} Options
13
+ * @property {string} [outDir]
14
+ * @property {string[]} [preBuild]
15
+ * @property {string[]} [postBuild]
16
+ */
17
+
18
+ /**
19
+ * @param {Options} options
20
+ */
21
+ export async function build({ outDir = 'dir', preBuild = [], postBuild = [] }) {
22
+ for (const cmd of preBuild) {
23
+ console.log(`Running pre-build command: ${cmd}`)
24
+ await $(cmd)
25
+ }
26
+ const cwd = process.cwd()
27
+ const start = performance.now()
28
+
29
+ const src = {
30
+ /** @type {string[]} */
31
+ layouts: [],
32
+ /** @type {string[]} */
33
+ components: [],
34
+ /** @type {string[]} */
35
+ pages: []
36
+ }
37
+
38
+ await walk(cwd, (file) => {
39
+ const fileName = path.basename(file)
40
+ if (match(fileName, /(.|_)layout.(j|t)sx$/)) {
41
+ src.layouts.push(file)
42
+ return
43
+ }
44
+ if (match(fileName, /(.|_)components.(j|t)sx$/)) {
45
+ src.components.push(file)
46
+ return
47
+ }
48
+ if (match(fileName, /\.(org)$/, /\.(j|t)sx$/)) {
49
+ src.pages.push(file)
50
+ return
51
+ }
52
+ })
53
+
54
+ const result = await esbuild.build({
55
+ entryPoints: [
56
+ ...Object.values(src.layouts),
57
+ ...src.components,
58
+ ...src.pages
59
+ ],
60
+ // entryNames: '[dir]/_/[name]',
61
+ bundle: true,
62
+ format: 'esm',
63
+ platform: 'node',
64
+ target: 'esnext',
65
+ jsx: 'automatic',
66
+ // write: false,
67
+ outdir: '.orga-build/js',
68
+ // splitting: true,
69
+ metafile: true,
70
+ plugins: [esbuildOrga(), rawLoader],
71
+ // external: ['react/jsx-runtime'],
72
+ loader: {
73
+ '.jsx': 'jsx',
74
+ '.tsx': 'tsx'
75
+ }
76
+ })
77
+
78
+ assert(result.metafile, 'metafile not found')
79
+
80
+ let components = {}
81
+
82
+ /**
83
+ * @typedef {Object} DirInfo
84
+ * @property {string|undefined} layout
85
+ * @property {PageInfo[]} pages
86
+
87
+ * @typedef {Object} PageInfo
88
+ * @property {Record<string, any>} metadata
89
+ * @property {import('react').ComponentType<any>} Content
90
+ * @property {string} src
91
+ * @property {string} file
92
+ */
93
+
94
+ /** @type {Record<string, DirInfo>} */
95
+ let map = {}
96
+ // iterate over results to get layout and components
97
+ for (const [file, meta] of Object.entries(result.metafile.outputs)) {
98
+ if (!meta.entryPoint) continue
99
+ const fullSrc = path.join(cwd, meta.entryPoint)
100
+ // get components
101
+ if (src.components.includes(fullSrc)) {
102
+ const module = await import(path.join(cwd, file))
103
+ components = { ...components, ...module }
104
+ continue
105
+ }
106
+
107
+ const dirPath = path.dirname(fullSrc)
108
+ map[dirPath] = map[dirPath] ?? {
109
+ layout: undefined,
110
+ pages: []
111
+ }
112
+ const dir = map[dirPath]
113
+
114
+ // get layouts
115
+ if (src.layouts.includes(fullSrc)) {
116
+ dir.layout = path.join(cwd, file)
117
+ continue
118
+ }
119
+ if (src.pages.includes(fullSrc)) {
120
+ const fullPath = path.join(cwd, file)
121
+ const { default: Content, ...metadata } = await import(fullPath)
122
+ dir.pages.push({
123
+ metadata: {
124
+ ...metadata,
125
+ slug: `/${meta.entryPoint.replace(/\.\w+$/, '.html')}`
126
+ },
127
+ Content,
128
+ src: fullSrc,
129
+ file: fullPath
130
+ })
131
+ }
132
+ }
133
+
134
+ for (const [, content] of Object.entries(map)) {
135
+ for (const page of content.pages) {
136
+ const Layout = await getLayout(page.src)
137
+ const e = createElement(
138
+ Layout ?? DefaultLayout,
139
+ { ...page.metadata, pages: content.pages.map((p) => p.metadata) },
140
+ createElement(page.Content, { components })
141
+ )
142
+ const html = renderToString(e)
143
+ // write to outDir/file
144
+ const relPath = path.relative(cwd, page.src)
145
+ const outPath = path.join(outDir, relPath)
146
+ const outDirPath = path.dirname(outPath)
147
+ await fs.mkdir(outDirPath, { recursive: true })
148
+ await fs
149
+ .writeFile(outPath.replace(/\.(org|jsx|tsx)$/, '.html'), html)
150
+ .catch(console.error)
151
+ }
152
+ }
153
+
154
+ for (const cmd of postBuild) {
155
+ console.log(`Running post-build command: ${cmd}`)
156
+ await $(cmd)
157
+ }
158
+
159
+ const end = performance.now()
160
+ console.log(`Built in ${(end - start).toFixed(2)}ms`)
161
+
162
+ /**
163
+ * @param {string} file
164
+ * @returns {Promise<import('react').ComponentType<any>|undefined>}
165
+ */
166
+ async function getLayout(file) {
167
+ if (file === cwd) return
168
+ const dir = path.dirname(file)
169
+ const ParentLayout = await getLayout(dir)
170
+ const { layout } = map[dir]
171
+ if (layout) {
172
+ const Layout = (await import(layout)).default
173
+ if (ParentLayout) {
174
+ return function (props) {
175
+ return createElement(
176
+ ParentLayout,
177
+ props,
178
+ createElement(Layout, props)
179
+ )
180
+ }
181
+ }
182
+ return Layout
183
+ }
184
+ return ParentLayout
185
+ }
186
+ }
187
+
188
+ /**
189
+ * Iterates over files in a directory recursively in a breadth-first manner.
190
+ * @param {string} dirPath - The path to the directory.
191
+ * @param {(file: string) => void} callback - The callback function to be called for each file.
192
+ */
193
+ async function walk(dirPath, callback) {
194
+ const queue = [dirPath]
195
+
196
+ const ignore = [/^\./, /node_modules/]
197
+
198
+ while (queue.length > 0) {
199
+ const currentPath = queue.shift()
200
+ if (!currentPath) break
201
+ const files = await fs.readdir(currentPath)
202
+
203
+ for (const file of files) {
204
+ if (match(file, ...ignore)) {
205
+ continue
206
+ }
207
+ const filePath = path.join(currentPath, file)
208
+ const stats = await fs.stat(filePath)
209
+
210
+ if (stats.isDirectory()) {
211
+ queue.push(filePath)
212
+ } else {
213
+ callback(filePath)
214
+ }
215
+ }
216
+ }
217
+ }
@@ -0,0 +1,13 @@
1
+ export default rawLoader;
2
+ declare namespace rawLoader {
3
+ let name: string;
4
+ /**
5
+ * @param {PluginBuild} build
6
+ * Build.
7
+ * @returns {undefined}
8
+ * Nothing.
9
+ */
10
+ function setup(build: PluginBuild): undefined;
11
+ }
12
+ import type { PluginBuild } from 'esbuild';
13
+ //# sourceMappingURL=raw-loader.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"raw-loader.d.ts","sourceRoot":"","sources":["raw-loader.js"],"names":[],"mappings":";;;IASC;;;;;OAKG;IACH,sBALW,WAAW,GAET,SAAS,CA+BrB;;iCA1C4B,SAAS"}
@@ -0,0 +1,47 @@
1
+ /**
2
+ * @import {PluginBuild} from 'esbuild'
3
+ */
4
+
5
+ import path from 'path'
6
+ import { promises as fs } from 'fs'
7
+
8
+ const rawLoader = {
9
+ name: 'raw',
10
+ /**
11
+ * @param {PluginBuild} build
12
+ * Build.
13
+ * @returns {undefined}
14
+ * Nothing.
15
+ */
16
+ setup(build) {
17
+ build.onResolve({ filter: /.*\?raw$/ }, (args) => {
18
+ return {
19
+ path: args.path,
20
+ pluginData: {
21
+ isAbsolute: path.isAbsolute(args.path),
22
+ resolveDir: args.resolveDir
23
+ },
24
+ namespace: 'raw-loader'
25
+ }
26
+ })
27
+
28
+ build.onLoad(
29
+ { filter: /.*\?raw$/, namespace: 'raw-loader' },
30
+ async (args) => {
31
+ const fullPath = args.pluginData.isAbsolute
32
+ ? args.path
33
+ : path.join(args.pluginData.resolveDir, args.path)
34
+ const contents = await fs.readFile(
35
+ fullPath.replace(/\?raw$/, ''),
36
+ 'utf8'
37
+ )
38
+ return {
39
+ contents,
40
+ loader: 'text'
41
+ }
42
+ }
43
+ )
44
+ }
45
+ }
46
+
47
+ export default rawLoader
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Evaluate an org/jsx/tsx/ts file. Returns as a module.
3
+ * It's like a dynamic import, but without relying on nodejs.
4
+ * @param {string} filePath
5
+ * @returns {Promise<any>}
6
+ */
7
+ export function evaluate(filePath: string): Promise<any>;
8
+ /**
9
+ * @param {string} pattern
10
+ */
11
+ export function build(pattern: string): Promise<esbuild.BuildResult<{
12
+ entryPoints: string[];
13
+ entryNames: string;
14
+ bundle: true;
15
+ format: "esm";
16
+ platform: "node";
17
+ target: string;
18
+ jsx: "automatic";
19
+ outdir: string;
20
+ plugins: {
21
+ name: string;
22
+ setup: (build: esbuild.PluginBuild) => undefined;
23
+ }[];
24
+ loader: {
25
+ '.jsx': "jsx";
26
+ '.tsx': "tsx";
27
+ };
28
+ }>>;
29
+ export function b(): Promise<void>;
30
+ import * as esbuild from 'esbuild';
31
+ //# sourceMappingURL=esbuild.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"esbuild.d.ts","sourceRoot":"","sources":["esbuild.js"],"names":[],"mappings":"AAIA;;;;;GAKG;AACH,mCAHW,MAAM,GACJ,OAAO,CAAC,GAAG,CAAC,CAwBxB;AAED;;GAEG;AACH,+BAFW,MAAM;;;;;;;;;;;;;;;;;IAmBhB;AAED,mCAA4B;yBAvDH,SAAS"}
package/lib/esbuild.js ADDED
@@ -0,0 +1,57 @@
1
+ import assert from 'assert'
2
+ import * as esbuild from 'esbuild'
3
+ import esbuildOrga from '@orgajs/esbuild'
4
+
5
+ /**
6
+ * Evaluate an org/jsx/tsx/ts file. Returns as a module.
7
+ * It's like a dynamic import, but without relying on nodejs.
8
+ * @param {string} filePath
9
+ * @returns {Promise<any>}
10
+ */
11
+ export async function evaluate(filePath) {
12
+ const result = await esbuild.build({
13
+ entryPoints: [filePath],
14
+ bundle: true,
15
+ format: 'esm',
16
+ platform: 'node',
17
+ target: 'esnext',
18
+ jsx: 'automatic',
19
+ write: false,
20
+ plugins: [esbuildOrga()],
21
+ loader: {
22
+ '.jsx': 'jsx',
23
+ '.tsx': 'tsx'
24
+ }
25
+ })
26
+
27
+ const files = result.outputFiles
28
+ assert(files.length === 1, 'Expected only one output file')
29
+ const code = files[0].text
30
+ return await new Function(
31
+ `return import("data:application/javascript,${encodeURIComponent(code)}")`
32
+ )()
33
+ }
34
+
35
+ /**
36
+ * @param {string} pattern
37
+ */
38
+ export async function build(pattern) {
39
+ return await esbuild.build({
40
+ entryPoints: [pattern],
41
+ entryNames: '[dir]/_/[name]',
42
+ bundle: true,
43
+ format: 'esm',
44
+ platform: 'node',
45
+ target: 'esnext',
46
+ jsx: 'automatic',
47
+ // write: false,
48
+ outdir: '.build',
49
+ plugins: [esbuildOrga()],
50
+ loader: {
51
+ '.jsx': 'jsx',
52
+ '.tsx': 'tsx'
53
+ }
54
+ })
55
+ }
56
+
57
+ export async function b() {}
@@ -0,0 +1,12 @@
1
+ /**
2
+ * @param {string} href
3
+ * URL.
4
+ * @param {unknown} context
5
+ * Context.
6
+ * @param {Function} defaultLoad
7
+ * Default `load`.
8
+ * @returns
9
+ * Result.
10
+ */
11
+ export function load(href: string, context: unknown, defaultLoad: Function): Promise<any>;
12
+ //# sourceMappingURL=jsx-loader.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"jsx-loader.d.ts","sourceRoot":"","sources":["jsx-loader.js"],"names":[],"mappings":"AAIA;;;;;;;;;GASG;AACH,2BATW,MAAM,WAEN,OAAO,uCAgCjB"}
@@ -0,0 +1,58 @@
1
+ import fs from 'node:fs/promises'
2
+ import { fileURLToPath } from 'node:url'
3
+ import { transform } from 'esbuild'
4
+
5
+ /**
6
+ * @param {string} href
7
+ * URL.
8
+ * @param {unknown} context
9
+ * Context.
10
+ * @param {Function} defaultLoad
11
+ * Default `load`.
12
+ * @returns
13
+ * Result.
14
+ */
15
+ export async function load(href, context, defaultLoad) {
16
+ const url = new URL(href)
17
+
18
+ const loader = getLoader(url.pathname)
19
+ if (!loader) {
20
+ return defaultLoad(href, context, defaultLoad)
21
+ }
22
+
23
+ const { code, warnings } = await transform(String(await fs.readFile(url)), {
24
+ format: 'esm',
25
+ loader,
26
+ sourcefile: fileURLToPath(url),
27
+ sourcemap: 'both',
28
+ target: 'esnext',
29
+ jsx: 'automatic',
30
+ })
31
+
32
+ if (warnings) {
33
+ for (const warning of warnings) {
34
+ console.log(warning.location)
35
+ console.log(warning.text)
36
+ }
37
+ }
38
+
39
+ return { format: 'module', shortCircuit: true, source: code }
40
+ }
41
+
42
+ /**
43
+ * @param {string} filename
44
+ * @returns {import('esbuild').Loader | null}
45
+ */
46
+ function getLoader(filename) {
47
+ const ext = filename.split('.').pop()
48
+ switch (ext) {
49
+ case 'jsx':
50
+ return 'jsx'
51
+ case 'tsx':
52
+ return 'tsx'
53
+ case 'ts':
54
+ return 'ts'
55
+ default:
56
+ return null
57
+ }
58
+ }
@@ -0,0 +1,3 @@
1
+ export const initialize: (options: Readonly<Options> | null | undefined) => Promise<void>;
2
+ export const load: (href: string, context: import("module").LoadHookContext, nextLoad: NextLoad) => Promise<import("module").LoadFnOutput>;
3
+ //# sourceMappingURL=orga-loader.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"orga-loader.d.ts","sourceRoot":"","sources":["orga-loader.js"],"names":[],"mappings":"AAOA,0FAA2C;AAC3C,2IAA+B"}
@@ -0,0 +1,9 @@
1
+ import { createLoader } from '@orgajs/node-loader'
2
+ import latex from 'rehype-katex'
3
+
4
+ let loader = createLoader({
5
+ rehypePlugins: [latex],
6
+ })
7
+
8
+ export const initialize = loader.initialize
9
+ export const load = loader.load
@@ -0,0 +1,12 @@
1
+ /**
2
+ * @param {string} href
3
+ * URL.
4
+ * @param {unknown} context
5
+ * Context.
6
+ * @param {Function} defaultLoad
7
+ * Default `load`.
8
+ * @returns
9
+ * Result.
10
+ */
11
+ export function load(href: string, context: unknown, defaultLoad: Function): Promise<any>;
12
+ //# sourceMappingURL=raw-loader.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"raw-loader.d.ts","sourceRoot":"","sources":["raw-loader.js"],"names":[],"mappings":"AAGA;;;;;;;;;GASG;AACH,2BATW,MAAM,WAEN,OAAO,uCAsBjB"}
@@ -0,0 +1,31 @@
1
+ import { readFileSync } from 'node:fs'
2
+ import { fileURLToPath } from 'node:url'
3
+
4
+ /**
5
+ * @param {string} href
6
+ * URL.
7
+ * @param {unknown} context
8
+ * Context.
9
+ * @param {Function} defaultLoad
10
+ * Default `load`.
11
+ * @returns
12
+ * Result.
13
+ */
14
+ const rawLoader = async (href, context, defaultLoad) => {
15
+ const url = new URL(href)
16
+ const { searchParams } = url
17
+ const raw = searchParams.get('raw')
18
+ if (raw === null || raw === undefined || raw.toLowerCase() === 'false') {
19
+ return defaultLoad(href, context, defaultLoad)
20
+ }
21
+
22
+ const filePath = fileURLToPath(href)
23
+ const fileContent = readFileSync(filePath, 'utf8')
24
+ return {
25
+ format: 'module',
26
+ shortCircuit: true,
27
+ source: `export default ${JSON.stringify(fileContent)}`
28
+ }
29
+ }
30
+
31
+ export const load = rawLoader
package/lib/serve.d.ts ADDED
@@ -0,0 +1,8 @@
1
+ /**
2
+ * @param {string} dir
3
+ */
4
+ export function serve(dir: string, port?: number): {
5
+ start: () => void;
6
+ stop: () => void;
7
+ };
8
+ //# sourceMappingURL=serve.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"serve.d.ts","sourceRoot":"","sources":["serve.js"],"names":[],"mappings":"AAGA;;GAEG;AACH,2BAFW,MAAM;;;EA2BhB"}
package/lib/serve.js ADDED
@@ -0,0 +1,32 @@
1
+ import handler from 'serve-handler'
2
+ import http from 'http'
3
+
4
+ /**
5
+ * @param {string} dir
6
+ */
7
+ export function serve(dir, port = 3000) {
8
+ /** @type {import('http').Server | null} */
9
+ let server = null
10
+
11
+ function start() {
12
+ server = http
13
+ .createServer((req, res) => {
14
+ return handler(req, res, {
15
+ public: dir,
16
+ })
17
+ })
18
+ .listen(port)
19
+ console.log(`Server running at http://localhost:${port}/`)
20
+ }
21
+
22
+ function stop() {
23
+ server?.close()
24
+ server?.on('close', () => {
25
+ server = null
26
+ return Promise.resolve()
27
+ })
28
+ }
29
+
30
+ start()
31
+ return { start, stop }
32
+ }
package/lib/util.d.ts ADDED
@@ -0,0 +1,23 @@
1
+ export function buildNav(): void;
2
+ /**
3
+ * @param {string} file
4
+ * @param {...RegExp|string} patterns
5
+ * @returns {boolean}
6
+ */
7
+ export function match(file: string, ...patterns: (RegExp | string)[]): boolean;
8
+ /**
9
+ * Default layout
10
+ * @param {Object} props
11
+ * @param {string|undefined} props.title
12
+ * @param {import('react').ReactNode} props.children
13
+ * @returns {React.JSX.Element}
14
+ */
15
+ export function DefaultLayout({ title, children }: {
16
+ title: string | undefined;
17
+ children: import("react").ReactNode;
18
+ }): React.JSX.Element;
19
+ /**
20
+ * @param {string} cmd
21
+ */
22
+ export function $(cmd: string): Promise<any>;
23
+ //# sourceMappingURL=util.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"util.d.ts","sourceRoot":"","sources":["util.js"],"names":[],"mappings":"AAGA,iCAA6B;AAE7B;;;;GAIG;AACH,4BAJW,MAAM,eACN,CAAG,MAAM,GAAC,MAAM,GAAA,GACd,OAAO,CASnB;AAED;;;;;;GAMG;AACH,mDAJG;IAAgC,KAAK,EAA7B,MAAM,GAAC,SAAS;IACiB,QAAQ,EAAzC,OAAO,OAAO,EAAE,SAAS;CACjC,GAAU,KAAK,CAAC,GAAG,CAAC,OAAO,CAkB7B;AAED;;GAEG;AACH,uBAFW,MAAM,gBAehB"}
package/lib/util.js ADDED
@@ -0,0 +1,61 @@
1
+ import { exec } from 'node:child_process'
2
+ import { createElement } from 'react'
3
+
4
+ export function buildNav() {}
5
+
6
+ /**
7
+ * @param {string} file
8
+ * @param {...RegExp|string} patterns
9
+ * @returns {boolean}
10
+ */
11
+ export function match(file, ...patterns) {
12
+ return patterns.some((p) => {
13
+ if (p instanceof RegExp) {
14
+ return p.test(file)
15
+ }
16
+ return file === p
17
+ })
18
+ }
19
+
20
+ /**
21
+ * Default layout
22
+ * @param {Object} props
23
+ * @param {string|undefined} props.title
24
+ * @param {import('react').ReactNode} props.children
25
+ * @returns {React.JSX.Element}
26
+ */
27
+ export function DefaultLayout({ title, children }) {
28
+ return createElement(
29
+ 'html',
30
+ { lang: 'en' },
31
+ createElement(
32
+ 'head',
33
+ {},
34
+ createElement('meta', { charSet: 'utf-8' }),
35
+ createElement('meta', {
36
+ name: 'viewport',
37
+ content: 'width=device-width, initial-scale=1'
38
+ }),
39
+ title && createElement('title', {}, title)
40
+ ),
41
+ createElement('body', {}, children)
42
+ )
43
+ }
44
+
45
+ /**
46
+ * @param {string} cmd
47
+ */
48
+ export async function $(cmd) {
49
+ return new Promise((resolve, reject) => {
50
+ exec(cmd, (err, stdout, stderr) => {
51
+ if (err) {
52
+ reject(err)
53
+ }
54
+ if (stderr) {
55
+ console.error(stderr)
56
+ }
57
+ console.log(stdout)
58
+ resolve(stdout)
59
+ })
60
+ })
61
+ }
package/lib/watch.d.ts ADDED
@@ -0,0 +1,10 @@
1
+ /**
2
+ * @param {import("fs").PathLike} dir
3
+ * @param {RegExp} ignore
4
+ * @param {{(event: fs.FileChangeInfo<string>): Promise<void> | void}} onChange
5
+ */
6
+ export function watch(dir: import("fs").PathLike, ignore: RegExp, onChange: {
7
+ (event: fs.FileChangeInfo<string>): Promise<void> | void;
8
+ }): Promise<void>;
9
+ import fs from 'node:fs/promises';
10
+ //# sourceMappingURL=watch.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"watch.d.ts","sourceRoot":"","sources":["watch.js"],"names":[],"mappings":"AAEA;;;;GAIG;AACH,2BAJW,OAAO,IAAI,EAAE,QAAQ,UACrB,MAAM,YACN;IAAC,CAAC,KAAK,EAAE,EAAE,CAAC,cAAc,CAAC,MAAM,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAA;CAAC,iBA+CpE;eApDc,kBAAkB"}
package/lib/watch.js ADDED
@@ -0,0 +1,53 @@
1
+ import fs from 'node:fs/promises'
2
+
3
+ /**
4
+ * @param {import("fs").PathLike} dir
5
+ * @param {RegExp} ignore
6
+ * @param {{(event: fs.FileChangeInfo<string>): Promise<void> | void}} onChange
7
+ */
8
+ export async function watch(dir, ignore, onChange) {
9
+ let busy = false
10
+ let dirty = false
11
+ /** @type {ReturnType<typeof setTimeout> | null} */
12
+ let timeout = null
13
+ const delay = 1000
14
+ const defaultIgnorePattern = /node_modules|\.git|\.DS_Store/
15
+
16
+ const watcher = fs.watch(dir, { recursive: true })
17
+ for await (const event of watcher) {
18
+ if (
19
+ event.eventType !== 'change' ||
20
+ event.filename === null ||
21
+ shouldIgnore(event.filename)
22
+ ) {
23
+ continue
24
+ }
25
+ console.log(`file changed: ${event.filename}`)
26
+ dirty = true
27
+ if (busy) {
28
+ continue
29
+ }
30
+ if (timeout !== null) clearTimeout(timeout)
31
+ timeout = setTimeout(processEvent, delay, event)
32
+ }
33
+
34
+ /**
35
+ * @param {any} event
36
+ */
37
+ async function processEvent(event) {
38
+ busy = true
39
+ dirty = false
40
+ await onChange(event)
41
+ busy = false
42
+ if (dirty) {
43
+ processEvent(event)
44
+ }
45
+ }
46
+
47
+ /**
48
+ * @param {string} filename
49
+ */
50
+ function shouldIgnore(filename) {
51
+ return defaultIgnorePattern.test(filename) || ignore.test(filename)
52
+ }
53
+ }
package/package.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "orga-build",
3
+ "version": "0.1.0",
4
+ "description": "A simple tool that builds org-mode files into a website",
5
+ "type": "module",
6
+ "bin": {
7
+ "orga-build": "cli.js"
8
+ },
9
+ "files": [
10
+ "lib/",
11
+ "cli.js",
12
+ "index.js",
13
+ "index.d.ts",
14
+ "index.d.ts.map"
15
+ ],
16
+ "exports": "./index.js",
17
+ "keywords": [
18
+ "orgajs",
19
+ "org-mode",
20
+ "build",
21
+ "website",
22
+ "react"
23
+ ],
24
+ "author": "Xiaoxing Hu <hi@xiaoxing.dev>",
25
+ "license": "MIT",
26
+ "dependencies": {
27
+ "@orgajs/esbuild": "^1.1.1",
28
+ "esbuild": "^0.24.2",
29
+ "globby": "^14.0.2",
30
+ "react": "^19.0.0",
31
+ "react-dom": "^19.0.0",
32
+ "rehype-katex": "^7.0.1",
33
+ "serve-handler": "^6.1.6",
34
+ "@orgajs/node-loader": "^1.1.1"
35
+ },
36
+ "devDependencies": {
37
+ "@types/node": "^22.13.1",
38
+ "@types/react": "^19.0.8",
39
+ "@types/react-dom": "^19.0.3",
40
+ "@types/serve-handler": "^6.1.4",
41
+ "@orgajs/orgx": "^2.5.0"
42
+ },
43
+ "scripts": {}
44
+ }