honox
Advanced tools
Comparing version 0.1.16 to 0.1.17
@@ -15,2 +15,5 @@ import { Hydrate, CreateElement, CreateChildren, TriggerHydration } from '../types.js'; | ||
ISLAND_FILES?: Record<string, () => Promise<unknown>>; | ||
/** | ||
* @deprecated | ||
*/ | ||
island_root?: string; | ||
@@ -17,0 +20,0 @@ }; |
@@ -11,9 +11,9 @@ import { render } from "hono/jsx/dom"; | ||
const FILES = options?.ISLAND_FILES ?? { | ||
...import.meta.glob("/app/islands/**/[a-zA-Z0-9[-]+.(tsx|ts)"), | ||
...import.meta.glob("/app/routes/**/_[a-zA-Z0-9[-]+.island.(tsx|ts)") | ||
...import.meta.glob("/app/islands/**/[a-zA-Z0-9-]+.tsx"), | ||
...import.meta.glob("/app/**/_[a-zA-Z0-9-]+.island.tsx"), | ||
...import.meta.glob("/app/**/$[a-zA-Z0-9-]+.tsx") | ||
}; | ||
const root = options?.island_root ?? "/app"; | ||
const hydrateComponent = async (document2) => { | ||
const filePromises = Object.keys(FILES).map(async (filePath) => { | ||
const componentName = filePath.replace(root, ""); | ||
const componentName = filePath; | ||
const elements = document2.querySelectorAll( | ||
@@ -44,3 +44,3 @@ `[${COMPONENT_NAME}="${componentName}"]:not([data-hono-hydrated])` | ||
createElement, | ||
async (name) => (await FILES[`${root}${name}`]()).default | ||
async (name) => (await FILES[`${name}`]()).default | ||
); | ||
@@ -47,0 +47,0 @@ } |
@@ -1,5 +0,5 @@ | ||
import { FC } from 'hono/jsx'; | ||
declare const HasIslands: ({ children }: { | ||
children: any; | ||
}) => any; | ||
declare const HasIslands: FC; | ||
export { HasIslands }; |
import { Fragment, jsx } from "hono/jsx/jsx-runtime"; | ||
import { useRequestContext } from "hono/jsx-renderer"; | ||
import { IMPORTING_ISLANDS_ID } from "../../constants.js"; | ||
import { contextStorage } from "../context-storage.js"; | ||
const HasIslands = ({ children }) => { | ||
const c = useRequestContext(); | ||
return /* @__PURE__ */ jsx(Fragment, { children: c.get(IMPORTING_ISLANDS_ID) ? children : /* @__PURE__ */ jsx(Fragment, {}) }); | ||
const c = contextStorage.getStore(); | ||
if (!c) { | ||
throw new Error("No context found"); | ||
} | ||
return /* @__PURE__ */ jsx(Fragment, { children: c.get(IMPORTING_ISLANDS_ID) && children }); | ||
}; | ||
@@ -8,0 +11,0 @@ export { |
export { HasIslands } from './has-islands.js'; | ||
export { Script } from './script.js'; | ||
import 'hono/jsx'; | ||
import 'vite'; |
@@ -1,2 +0,1 @@ | ||
import { FC } from 'hono/jsx'; | ||
import { Manifest } from 'vite'; | ||
@@ -11,4 +10,4 @@ | ||
}; | ||
declare const Script: FC<Options>; | ||
declare const Script: (options: Options) => any; | ||
export { Script }; |
import { Fragment, jsx } from "hono/jsx/jsx-runtime"; | ||
import { HasIslands } from "./has-islands.js"; | ||
const Script = async (options) => { | ||
const Script = (options) => { | ||
const src = options.src; | ||
@@ -5,0 +5,0 @@ if (options.prod ?? import.meta.env.PROD) { |
@@ -8,3 +8,2 @@ export { createApp } from './with-defaults.js'; | ||
import '../constants.js'; | ||
import 'hono/jsx'; | ||
import 'vite'; |
import { Hono } from "hono"; | ||
import { createMiddleware } from "hono/factory"; | ||
import { IMPORTING_ISLANDS_ID } from "../constants.js"; | ||
import { contextStorage } from "./context-storage.js"; | ||
import { | ||
@@ -9,3 +10,3 @@ filePathToPath, | ||
sortDirectoriesByDepth | ||
} from "../utils/file.js"; | ||
} from "./utils/file.js"; | ||
const NOTFOUND_FILENAME = "_404.tsx"; | ||
@@ -20,2 +21,5 @@ const ERROR_FILENAME = "_error.tsx"; | ||
const trailingSlash = options.trailingSlash ?? false; | ||
app.use(async function ShareContext(c, next) { | ||
await contextStorage.run(c, () => next()); | ||
}); | ||
if (options.init) { | ||
@@ -22,0 +26,0 @@ options.init(app); |
import { Plugin } from 'vite'; | ||
declare function injectImportingIslands(): Promise<Plugin>; | ||
type InjectImportingIslandsOptions = { | ||
appDir?: string; | ||
islandDir?: string; | ||
}; | ||
declare function injectImportingIslands(options?: InjectImportingIslandsOptions): Promise<Plugin>; | ||
export { injectImportingIslands }; |
@@ -8,9 +8,11 @@ import { readFile } from "fs/promises"; | ||
import { IMPORTING_ISLANDS_ID } from "../constants.js"; | ||
import { matchIslandComponentId } from "./utils/path.js"; | ||
const generate = _generate.default ?? _generate; | ||
async function injectImportingIslands() { | ||
const isIslandRegex = new RegExp(/(\/islands\/|\_[a-zA-Z0-9[-]+\.island\.[tj]sx$)/); | ||
const fileRegex = new RegExp(/(routes|_renderer|_error|_404)\/.*\.[tj]sx$/); | ||
async function injectImportingIslands(options) { | ||
let appPath = ""; | ||
const islandDir = options?.islandDir ?? "/app/islands"; | ||
let root = ""; | ||
const cache = {}; | ||
const walkDependencyTree = async (baseFile, dependencyFile) => { | ||
const depPath = dependencyFile ? path.join(path.dirname(baseFile), dependencyFile) + ".tsx" : baseFile; | ||
const walkDependencyTree = async (baseFile, resolve, dependencyFile) => { | ||
const depPath = dependencyFile ? typeof dependencyFile === "string" ? path.join(path.dirname(baseFile), dependencyFile) + ".tsx" : dependencyFile["id"] : baseFile; | ||
const deps = [depPath]; | ||
@@ -25,3 +27,6 @@ try { | ||
const childDeps = await Promise.all( | ||
currentFileDeps.map(async (x) => await walkDependencyTree(depPath, x)) | ||
currentFileDeps.map(async (file) => { | ||
const resolvedId = await resolve(file, baseFile); | ||
return await walkDependencyTree(depPath, resolve, resolvedId ?? file); | ||
}) | ||
); | ||
@@ -36,7 +41,16 @@ deps.push(...childDeps.flat()); | ||
name: "inject-importing-islands", | ||
configResolved: async (config) => { | ||
appPath = path.join(config.root, options?.appDir ?? "/app"); | ||
root = config.root; | ||
}, | ||
async transform(sourceCode, id) { | ||
if (!fileRegex.test(id)) { | ||
if (!path.resolve(id).startsWith(appPath)) { | ||
return; | ||
} | ||
const hasIslandsImport = (await walkDependencyTree(id)).flat().some((x) => isIslandRegex.test(normalizePath(x))); | ||
const hasIslandsImport = (await Promise.all( | ||
(await walkDependencyTree(id, async (id2) => await this.resolve(id2))).flat().map(async (x) => { | ||
const rootPath = "/" + path.relative(root, normalizePath(x)).replace(/\\/g, "/"); | ||
return matchIslandComponentId(rootPath, islandDir); | ||
}) | ||
)).some((matched) => matched); | ||
if (!hasIslandsImport) { | ||
@@ -43,0 +57,0 @@ return; |
@@ -6,3 +6,7 @@ import { Plugin } from 'vite'; | ||
type IslandComponentsOptions = { | ||
/** | ||
* @deprecated | ||
*/ | ||
isIsland?: IsIsland; | ||
islandDir?: string; | ||
reactApiImportSource?: string; | ||
@@ -9,0 +13,0 @@ }; |
@@ -32,5 +32,3 @@ import fs from "fs/promises"; | ||
import { parse as parseJsonc } from "jsonc-parser"; | ||
function isComponentName(name) { | ||
return /^[A-Z][A-Z0-9]*[a-z][A-Za-z0-9]*$/.test(name); | ||
} | ||
import { matchIslandComponentId, isComponentName } from "./utils/path.js"; | ||
function addSSRCheck(funcName, componentName, componentExport) { | ||
@@ -162,2 +160,3 @@ const isSSR = memberExpression( | ||
let reactApiImportSource = options?.reactApiImportSource; | ||
const islandDir = options?.islandDir ?? "/app/islands"; | ||
return { | ||
@@ -182,3 +181,3 @@ name: "transform-island-components", | ||
async load(id) { | ||
if (/\/honox\/.*?\/vite\/components\//.test(id)) { | ||
if (/\/honox\/.*?\/(?:server|vite)\/components\//.test(id)) { | ||
if (!reactApiImportSource) { | ||
@@ -193,11 +192,4 @@ return; | ||
} | ||
const defaultIsIsland = (id2) => { | ||
const islandDirectoryPath = path.join(root, "app"); | ||
return path.resolve(id2).startsWith(islandDirectoryPath); | ||
}; | ||
const matchIslandPath = options?.isIsland ?? defaultIsIsland; | ||
if (!matchIslandPath(id)) { | ||
return; | ||
} | ||
const match = id.match(/(\/islands\/.+?\.tsx$)|(\/routes\/.*\_[a-zA-Z0-9[-]+\.island\.tsx$)/); | ||
const rootPath = "/" + path.relative(root, id).replace(/\\/g, "/"); | ||
const match = matchIslandComponentId(rootPath, islandDir); | ||
if (match) { | ||
@@ -204,0 +196,0 @@ const componentName = match[0]; |
@@ -0,3 +1,5 @@ | ||
import fs from "fs"; | ||
import os from "os"; | ||
import path from "path"; | ||
import { transformJsxTags, islandComponents } from "./island-components"; | ||
import { transformJsxTags, islandComponents } from "./island-components.js"; | ||
describe("transformJsxTags", () => { | ||
@@ -218,20 +220,44 @@ it("Should add component-wrapper and component-name attribute", () => { | ||
describe("reactApiImportSource", () => { | ||
const component = path.resolve(__dirname, "../vite/components/honox-island.tsx").replace(/\\/g, "/"); | ||
it("use 'hono/jsx' by default", async () => { | ||
const plugin = islandComponents(); | ||
await plugin.configResolved({ root: "root" }); | ||
const res = await plugin.load(component); | ||
expect(res.code).toMatch(/'hono\/jsx'/); | ||
expect(res.code).not.toMatch(/'react'/); | ||
describe("vite/components", () => { | ||
const component = path.resolve(__dirname, "../vite/components/honox-island.tsx").replace(/\\/g, "/"); | ||
it("use 'hono/jsx' by default", async () => { | ||
const plugin = islandComponents(); | ||
await plugin.configResolved({ root: "root" }); | ||
const res = await plugin.load(component); | ||
expect(res.code).toMatch(/'hono\/jsx'/); | ||
expect(res.code).not.toMatch(/'react'/); | ||
}); | ||
it("enable to specify 'react'", async () => { | ||
const plugin = islandComponents({ | ||
reactApiImportSource: "react" | ||
}); | ||
await plugin.configResolved({ root: "root" }); | ||
const res = await plugin.load(component); | ||
expect(res.code).not.toMatch(/'hono\/jsx'/); | ||
expect(res.code).toMatch(/'react'/); | ||
}); | ||
}); | ||
it("enable to specify 'react'", async () => { | ||
const plugin = islandComponents({ | ||
reactApiImportSource: "react" | ||
describe("server/components", () => { | ||
const tmpdir = os.tmpdir(); | ||
const component = path.resolve(tmpdir, "honox/dist/server/components/has-islands.js").replace(/\\/g, "/"); | ||
fs.mkdirSync(path.dirname(component), { recursive: true }); | ||
fs.writeFileSync(component, "import { jsx } from 'hono/jsx/jsx-runtime'"); | ||
it("use 'hono/jsx' by default", async () => { | ||
const plugin = islandComponents(); | ||
await plugin.configResolved({ root: "root" }); | ||
const res = await plugin.load(component); | ||
expect(res.code).toMatch(/'hono\/jsx\/jsx-runtime'/); | ||
expect(res.code).not.toMatch(/'react\/jsx-runtime'/); | ||
}); | ||
await plugin.configResolved({ root: "root" }); | ||
const res = await plugin.load(component); | ||
expect(res.code).not.toMatch(/'hono\/jsx'/); | ||
expect(res.code).toMatch(/'react'/); | ||
it("enable to specify 'react'", async () => { | ||
const plugin = islandComponents({ | ||
reactApiImportSource: "react" | ||
}); | ||
await plugin.configResolved({ root: "root" }); | ||
const res = await plugin.load(component); | ||
expect(res.code).not.toMatch(/'hono\/jsx\/jsx-runtime'/); | ||
expect(res.code).toMatch(/'react\/jsx-runtime'/); | ||
}); | ||
}); | ||
}); | ||
}); |
{ | ||
"name": "honox", | ||
"version": "0.1.16", | ||
"version": "0.1.17", | ||
"main": "dist/index.js", | ||
@@ -115,3 +115,3 @@ "type": "module", | ||
"@babel/types": "^7.23.6", | ||
"@hono/vite-dev-server": "^0.12.0", | ||
"@hono/vite-dev-server": "^0.12.1", | ||
"jsonc-parser": "^3.2.1", | ||
@@ -132,3 +132,3 @@ "precinct": "^12.0.2" | ||
"glob": "^10.3.10", | ||
"hono": "^4.3.2", | ||
"hono": "^4.3.4", | ||
"np": "7.7.0", | ||
@@ -135,0 +135,0 @@ "prettier": "^3.1.1", |
@@ -316,3 +316,3 @@ # HonoX | ||
If you want to add a `nonce` attribute to `<Script />`, `<script />`, or `<style />` element, you can use [Security Headers Middleware](https://hono.dev/middleware/builtin/secure-headers). | ||
If you want to add a `nonce` attribute to `<Script />` or `<script />` element, you can use [Security Headers Middleware](https://hono.dev/middleware/builtin/secure-headers). | ||
@@ -326,10 +326,9 @@ Define the middleware: | ||
export default createRoute( | ||
secureHeaders({ | ||
contentSecurityPolicy: { | ||
scriptSrc: [NONCE], | ||
styleSrc: [NONCE], | ||
}, | ||
}) | ||
) | ||
secureHeaders({ | ||
contentSecurityPolicy: import.meta.env.PROD | ||
? { | ||
scriptSrc: [NONCE], | ||
} | ||
: undefined, | ||
}) | ||
``` | ||
@@ -371,3 +370,3 @@ | ||
- Placed under `app/islands` directory or named with `_` prefix and `island.tsx` suffix like `_componentName.island.tsx`. | ||
- Placed under `app/islands` directory or named with `$` prefix like `$componentName.tsx`. | ||
- It should be exported as a `default` or a proper component name that uses camel case but does not contain `_` and is not all uppercase. | ||
@@ -754,3 +753,4 @@ | ||
name = "my-project-name" | ||
compatibility_date = "2023-12-01" | ||
compatibility_date = "2024-04-01" | ||
compatibility_flags = [ "nodejs_compat" ] | ||
pages_build_output_dir = "./dist" | ||
@@ -790,2 +790,12 @@ | ||
Add the `wrangler.toml`: | ||
```toml | ||
# wrangler.toml | ||
name = "my-project-name" | ||
compatibility_date = "2024-04-01" | ||
compatibility_flags = [ "nodejs_compat" ] | ||
pages_build_output_dir = "./dist" | ||
``` | ||
Setup the `vite.config.ts`: | ||
@@ -835,3 +845,3 @@ | ||
```txt | ||
wrangler pages deploy ./dist | ||
wrangler pages deploy | ||
``` | ||
@@ -838,0 +848,0 @@ |
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 1 instance in 1 package
104436
67
2061
916
3