nowebcli 0.2.1

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,10 @@
1
+ "use client";
2
+
3
+ import { Shader as BaseShader } from "shaders/react";
4
+ import type { ComponentProps } from "react";
5
+
6
+ export * from "shaders/react";
7
+
8
+ export function Shader(props: ComponentProps<typeof BaseShader>) {
9
+ return <BaseShader {...props} disableTelemetry={true} />;
10
+ }
@@ -0,0 +1,200 @@
1
+ import { execFile } from "node:child_process";
2
+ import { existsSync } from "node:fs";
3
+ import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
4
+ import { tmpdir } from "node:os";
5
+ import path from "node:path";
6
+ import { fileURLToPath } from "node:url";
7
+ import { promisify } from "node:util";
8
+ import type { ComponentType } from "react";
9
+ import type { CustomizationValue, TemplateCustomization } from "../lib/customizations";
10
+ import { compileTailwindForTemplate } from "./compileTailwind.ts";
11
+ import { validateTemplateCode } from "./validateTemplateCode.ts";
12
+
13
+ const run = promisify(execFile);
14
+ const templatesPath = path.dirname(fileURLToPath(import.meta.url));
15
+ const packageModulesPath = path.join(path.dirname(templatesPath), "node_modules");
16
+ const modulesPath = existsSync(path.join(packageModulesPath, ".bin", "esbuild"))
17
+ ? packageModulesPath
18
+ : path.join(process.cwd(), "node_modules");
19
+
20
+ const TEMPLATE_COMPILE_TIMEOUT_MS = 10_000;
21
+ const TEMPLATE_COMPILE_MAX_BUFFER = 8 * 1024 * 1024;
22
+ const TEMPLATE_RUNTIME_IMPORT = "noweb";
23
+ const TEMPLATE_RUNTIME_IMPORTS = [TEMPLATE_RUNTIME_IMPORT, "@noweb"];
24
+ const SHADERS_RUNTIME_IMPORT = "@noweb/shaders";
25
+
26
+ export type TemplateCustomizationDefinition = Pick<TemplateCustomization, "fieldId" | "name" | "defaultValue">;
27
+
28
+ type CompiledTemplate = {
29
+ component?: ComponentType<Record<string, unknown>>;
30
+ customizations?: unknown[];
31
+ customization?: unknown[];
32
+ };
33
+
34
+ export async function compileTemplate(
35
+ code: string,
36
+ ): Promise<ComponentType<Record<string, unknown>> | CompiledTemplate> {
37
+ validateTemplateCode(code);
38
+ const dir = await mkdtemp(path.join(tmpdir(), "template-"));
39
+ const input = path.join(dir, "template.tsx");
40
+ const output = path.join(dir, "bundle.mjs");
41
+
42
+ try {
43
+ await writeFile(input, withRuntimeImports(code));
44
+ const nodePath = modulesPath;
45
+
46
+ await runTemplateCommand(path.join(nodePath, ".bin", "esbuild"), [
47
+ input,
48
+ "--bundle",
49
+ "--format=esm",
50
+ "--platform=browser",
51
+ ...runtimeAliases(),
52
+ `--outfile=${output}`,
53
+ ], nodePath);
54
+
55
+ const dynamicImport = new Function("specifier", "return import(specifier)") as (
56
+ specifier: string,
57
+ ) => Promise<{ default: unknown }>;
58
+ const compiled = await dynamicImport(`file://${output}?t=${Date.now()}`);
59
+ const template = compiled.default as CompiledTemplate;
60
+ if (!template) throw new Error("Template must default-export a component or object.");
61
+ return template.component ?? template;
62
+ } finally {
63
+ await rm(dir, { force: true, recursive: true });
64
+ }
65
+ }
66
+
67
+ export async function compileTemplateBundle(
68
+ code: string,
69
+ customizations: TemplateCustomizationDefinition[] = [],
70
+ assetBase = "/assets",
71
+ ) {
72
+ validateTemplateCode(code);
73
+ const dir = await mkdtemp(path.join(tmpdir(), "template-"));
74
+ const input = path.join(dir, "template.tsx");
75
+ const entry = path.join(dir, "entry.tsx");
76
+ const output = path.join(dir, "bundle.js");
77
+
78
+ try {
79
+ await writeFile(input, withRuntimeImports(code));
80
+ await writeFile(
81
+ entry,
82
+ `
83
+ import React from "react";
84
+ import { createRoot } from "react-dom/client";
85
+ import template from "./template";
86
+ import {
87
+ configureTemplateRuntime,
88
+ TemplateRuntimeProvider,
89
+ } from "${TEMPLATE_RUNTIME_IMPORT}";
90
+
91
+ const Component = template?.component ?? template;
92
+ const customizationDefaults = ${JSON.stringify(customizationDefaults(customizations))};
93
+ let root;
94
+
95
+ window.__nowebRenderTemplate = (element, props) => {
96
+ if (!Component) {
97
+ element.textContent = "Template must default-export a component or object.";
98
+ return () => {};
99
+ }
100
+ root ??= createRoot(element);
101
+ const values = props?.customizations ?? props?.userCustomizations ?? props?.uc ?? null;
102
+ configureTemplateRuntime(values, customizationDefaults, {
103
+ assetBase: ${JSON.stringify(assetBase)},
104
+ dataApiBase: props?.dataApiBase,
105
+ projectId: props?.projectId == null ? undefined : String(props.projectId),
106
+ });
107
+ root.render(
108
+ React.createElement(
109
+ TemplateRuntimeProvider,
110
+ { customizations: values, defaults: customizationDefaults },
111
+ React.createElement(Component, props),
112
+ ),
113
+ );
114
+ return () => {};
115
+ };
116
+ `,
117
+ );
118
+
119
+ const nodePath = modulesPath;
120
+
121
+ await runTemplateCommand(path.join(nodePath, ".bin", "esbuild"), [
122
+ entry,
123
+ "--bundle",
124
+ "--format=iife",
125
+ "--minify",
126
+ "--platform=browser",
127
+ ...runtimeAliases(),
128
+ `--outfile=${output}`,
129
+ ], nodePath);
130
+
131
+ return await import("node:fs/promises").then((fs) => fs.readFile(output, "utf8"));
132
+ } finally {
133
+ await rm(dir, { force: true, recursive: true });
134
+ }
135
+ }
136
+
137
+ export async function compileTemplateAssets(
138
+ code: string,
139
+ customizations: TemplateCustomizationDefinition[] = [],
140
+ assetBase = "/assets",
141
+ ) {
142
+ validateTemplateCode(code);
143
+ const runtimeSource = await readFile(templateRuntimePath(), "utf8");
144
+ const [js, css] = await Promise.all([
145
+ compileTemplateBundle(code, customizations, assetBase),
146
+ compileTailwindForTemplate(code, [runtimeSource]),
147
+ ]);
148
+
149
+ return { js, css };
150
+ }
151
+
152
+ function customizationDefaults(
153
+ customizations: TemplateCustomizationDefinition[],
154
+ ) {
155
+ return Object.fromEntries(
156
+ customizations.flatMap((field) => {
157
+ const byId: [string, CustomizationValue] = [String(field.fieldId), field.defaultValue];
158
+ const name = field.name.trim();
159
+ return name
160
+ ? [byId, [name, field.defaultValue] as [string, CustomizationValue]]
161
+ : [byId];
162
+ }),
163
+ );
164
+ }
165
+
166
+ function runtimeAliases() {
167
+ return [
168
+ ...TEMPLATE_RUNTIME_IMPORTS.map(
169
+ (moduleName) => `--alias:${moduleName}=${templateRuntimePath()}`,
170
+ ),
171
+ `--alias:${SHADERS_RUNTIME_IMPORT}=${shadersRuntimePath()}`,
172
+ ];
173
+ }
174
+
175
+ function templateRuntimePath() {
176
+ return path.join(templatesPath, "templateRuntime.tsx");
177
+ }
178
+
179
+ function shadersRuntimePath() {
180
+ return path.join(templatesPath, "shadersRuntime.tsx");
181
+ }
182
+
183
+ function withRuntimeImports(code: string) {
184
+ return code.replace(/(["'])shaders\/react\1/g, `$1${SHADERS_RUNTIME_IMPORT}$1`);
185
+ }
186
+
187
+ async function runTemplateCommand(
188
+ command: string,
189
+ args: string[],
190
+ nodePath: string,
191
+ ) {
192
+ return run(command, args, {
193
+ env: {
194
+ ...process.env,
195
+ NODE_PATH: [nodePath, process.env.NODE_PATH].filter(Boolean).join(path.delimiter),
196
+ },
197
+ maxBuffer: TEMPLATE_COMPILE_MAX_BUFFER,
198
+ timeout: TEMPLATE_COMPILE_TIMEOUT_MS,
199
+ });
200
+ }
@@ -0,0 +1,280 @@
1
+ import React, {
2
+ type ComponentProps,
3
+ createContext,
4
+ useCallback,
5
+ useContext,
6
+ type ReactNode,
7
+ } from "react";
8
+ import {
9
+ DynamicIcon,
10
+ iconNames,
11
+ type IconName,
12
+ } from "lucide-react/dynamic";
13
+ import ReactMarkdown, { type Components } from "react-markdown";
14
+ import remarkGfm from "remark-gfm";
15
+ import {
16
+ normalizeCustomizationValues,
17
+ type CustomizationValues,
18
+ } from "../lib/customizations";
19
+
20
+ export type { CustomizationValue, CustomizationValues } from "../lib/customizations";
21
+
22
+ export type TemplateProps = {
23
+ customizations?: CustomizationValues | null;
24
+ };
25
+
26
+ type TemplateRuntimeContextValue = {
27
+ assetBase?: string;
28
+ dataApiBase?: string;
29
+ defaults: CustomizationValues;
30
+ projectId?: string;
31
+ values: CustomizationValues;
32
+ };
33
+
34
+ const TemplateRuntimeContext = createContext<TemplateRuntimeContextValue>({
35
+ defaults: {},
36
+ values: {},
37
+ });
38
+ let currentTemplateRuntime: TemplateRuntimeContextValue = {
39
+ defaults: {},
40
+ values: {},
41
+ };
42
+
43
+ export function TemplateRuntimeProvider({
44
+ children,
45
+ customizations,
46
+ defaults,
47
+ }: {
48
+ children: ReactNode;
49
+ customizations?: CustomizationValues | null;
50
+ defaults?: CustomizationValues | null;
51
+ }) {
52
+ const runtime = {
53
+ defaults: normalizeValues(defaults),
54
+ values: normalizeValues(customizations),
55
+ };
56
+
57
+ return (
58
+ <TemplateRuntimeContext.Provider value={runtime}>
59
+ {children}
60
+ </TemplateRuntimeContext.Provider>
61
+ );
62
+ }
63
+
64
+ export function configureTemplateRuntime(
65
+ customizations?: CustomizationValues | null,
66
+ defaults?: CustomizationValues | null,
67
+ project?: { assetBase?: string; dataApiBase?: string; projectId?: string },
68
+ ) {
69
+ currentTemplateRuntime = {
70
+ assetBase: project?.assetBase,
71
+ dataApiBase: project?.dataApiBase,
72
+ defaults: normalizeValues(defaults),
73
+ projectId: project?.projectId,
74
+ values: normalizeValues(customizations),
75
+ };
76
+ }
77
+
78
+ export function asset(path: string) {
79
+ const relativePath = path.replace(/^\/+/, "");
80
+ if (!relativePath || relativePath.split("/").includes("..")) {
81
+ throw new Error("asset() requires a relative path inside the template public folder");
82
+ }
83
+ const base = currentTemplateRuntime.assetBase?.replace(/\/$/, "") || "/assets";
84
+ return `${base}/${relativePath.split("/").map(encodeURIComponent).join("/")}`;
85
+ }
86
+
87
+ export type ProjectDataRecord<T extends Record<string, unknown> = Record<string, unknown>> = {
88
+ id: string;
89
+ data: T;
90
+ };
91
+
92
+ class ProjectDataRequestError extends Error {
93
+ constructor(message: string, readonly status: number) {
94
+ super(message);
95
+ }
96
+ }
97
+
98
+ export const db = {
99
+ async isAdmin(adminName: string) {
100
+ try {
101
+ await projectDataRequest("?admin=1", { adminName });
102
+ return true;
103
+ } catch (error) {
104
+ if (error instanceof ProjectDataRequestError && error.status === 401) return false;
105
+ throw error;
106
+ }
107
+ },
108
+ list<T extends Record<string, unknown> = Record<string, unknown>>(adminName?: string) {
109
+ return projectDataRequest<ProjectDataRecord<T>[]>(adminName ? "?admin=1" : "", { adminName });
110
+ },
111
+ create<T extends Record<string, unknown>>(data: T, adminName: string) {
112
+ return projectDataRequest<ProjectDataRecord<T>>("", { adminName, body: data, method: "POST" });
113
+ },
114
+ update<T extends Record<string, unknown>>(id: string | number, data: T, adminName: string) {
115
+ return projectDataRequest<ProjectDataRecord<T>>(`?id=${id}`, { adminName, body: data, method: "PATCH" });
116
+ },
117
+ remove(id: string | number, adminName: string) {
118
+ return projectDataRequest<void>(`?id=${id}`, { adminName, method: "DELETE" });
119
+ },
120
+ claim<T extends Record<string, unknown>>(id: string | number, data: T) {
121
+ return projectDataRequest<ProjectDataRecord<T>>(`?id=${id}`, { body: { claim: data }, method: "PATCH" });
122
+ },
123
+ };
124
+
125
+ async function projectDataRequest<T>(
126
+ path: string,
127
+ options: { adminName?: string; body?: Record<string, unknown>; method?: string },
128
+ ) {
129
+ const { dataApiBase, projectId } = currentTemplateRuntime;
130
+ if (!projectId) throw new Error("Database access is available inside projects only");
131
+
132
+ const response = await fetch(
133
+ `${(dataApiBase || "https://noweb.app").replace(/\/$/, "")}/api/project-data/${encodeURIComponent(projectId)}${path}`,
134
+ {
135
+ method: options.method || "GET",
136
+ headers: {
137
+ ...(options.body ? { "Content-Type": "application/json" } : {}),
138
+ ...(options.adminName ? { "X-Admin-Name": options.adminName } : {}),
139
+ },
140
+ body: options.body ? JSON.stringify(options.body) : undefined,
141
+ },
142
+ );
143
+ const result = response.status === 204 ? null : await response.json().catch(() => null);
144
+ if (!response.ok) {
145
+ throw new ProjectDataRequestError(result?.error || `Database request failed (${response.status})`, response.status);
146
+ }
147
+ return result as T;
148
+ }
149
+
150
+ export function custom<T = string>(fieldId: number): T {
151
+ if (arguments.length !== 1) {
152
+ throw new Error("custom() accepts exactly one argument");
153
+ }
154
+ const value = resolveCustomization(currentTemplateRuntime, fieldId);
155
+ return (value === undefined ? "" : value) as T;
156
+ }
157
+
158
+ export function useCustomization(fieldId: number): string {
159
+ const value = resolveCustomization(useContext(TemplateRuntimeContext), fieldId);
160
+ return typeof value === "string" ? value : "";
161
+ }
162
+
163
+ export function useCustomizations() {
164
+ const context = useContext(TemplateRuntimeContext);
165
+ return useCallback(
166
+ (fieldId: number) => {
167
+ const value = resolveCustomization(context, fieldId);
168
+ return typeof value === "string" ? value : "";
169
+ },
170
+ [context],
171
+ );
172
+ }
173
+
174
+ export function CustomIcon({
175
+ fieldId,
176
+ fallbackName = "circle",
177
+ ...props
178
+ }: Omit<ComponentProps<typeof DynamicIcon>, "name"> & {
179
+ fieldId: number;
180
+ fallbackName?: IconName;
181
+ }) {
182
+ const value = useCustomization(fieldId);
183
+ const name = iconNames.includes(value as IconName) ? value as IconName : fallbackName;
184
+ return <DynamicIcon name={name} {...props} />;
185
+ }
186
+
187
+ export function Markdown({ text = "" }: { text?: string }) {
188
+ return (
189
+ <span className="mt-10 block space-y-4 text-base leading-7">
190
+ <ReactMarkdown components={markdownComponents} remarkPlugins={[remarkGfm]}>
191
+ {text}
192
+ </ReactMarkdown>
193
+ </span>
194
+ );
195
+ }
196
+
197
+ export function markdown(text = "") {
198
+ return <Markdown text={text} />;
199
+ }
200
+
201
+ const SKILL_ICON_ALIASES: Record<string, string> = {
202
+ ae: "aftereffects", ai: "illustrator", amazonwebservices: "aws", au: "audition",
203
+ bots: "discordbots", cf: "cloudflare", express: "expressjs", gatsbyjs: "gatsby",
204
+ ghactions: "githubactions", go: "golang", googlecloud: "gcp", gql: "graphql",
205
+ js: "javascript", k8s: "kubernetes", ktorio: "ktor", md: "markdown",
206
+ mongo: "mongodb", mui: "materialui", nest: "nestjs", net: "dotnet",
207
+ next: "nextjs", nuxt: "nuxtjs", postgres: "postgresql", pr: "premiere",
208
+ ps: "photoshop", pwsh: "powershell", py: "python", rollup: "rollupjs",
209
+ rxjava: "reactivex", rxjs: "reactivex", sc: "scala", scss: "sass",
210
+ sklearn: "scikitlearn", tailwind: "tailwindcss", ts: "typescript",
211
+ unreal: "unrealengine", vlang: "v", vue: "vuejs", wasm: "webassembly",
212
+ windi: "windicss",
213
+ };
214
+
215
+ const LIGHT_SKILL_ICONS = new Set(`ableton activitypub actix aiscript alpinejs anaconda androidstudio angular apple arch autocad aws azure bash bevy bitbucket blender bsd bun cassandra clion clojure cloudflare cmake codepen coffeescript crystal cypress d3 dart debian deno devto discordjs dynamodb eclipse elasticsearch elixir elysia emotion expressjs fediverse figma firebase flask flutter gcp gherkin github githubactions gitlab gmail godot gradle grafana graphql gtk haskell haxe haxeflixel hibernate htmx idea ipfs java jenkins julia kali kotlin ktor laravel latex less linux lit lua markdown mastodon materialui matlab maven mint misskey mysql neovim nestjs netlify nextjs nim nix nodejs notion npm nuxtjs obsidian octave opencv openstack php phpstorm pinia pkl plan9 planetscale pnpm postgresql powershell processing pug pycharm python pytorch qt r rabbitmq raspberrypi react reactivex redhat redis regex remix replit rider rollupjs ros scala scikitlearn sequelize sketchup solidjs spotify spring stackoverflow sublime supabase svg symfony tailwindcss tauri tensorflow terraform threejs ubuntu unity v vercel vim visualstudio vite vitest vscode vscodium vuejs vuetify webpack webstorm windicss windows workers yarn yew zig`.split(" "));
216
+
217
+ export function skillIcon(name: string, theme: "dark" | "light" = "dark") {
218
+ const key = name.trim().toLowerCase();
219
+ const icon = SKILL_ICON_ALIASES[key] ?? key;
220
+ if (!/^[a-z0-9]+$/.test(icon)) return "";
221
+ const variant = theme === "light" && LIGHT_SKILL_ICONS.has(icon) ? "-light" : "";
222
+ return `https://storage.noweb.app/noweb/skillIcons/${icon}${variant}.svg`;
223
+ }
224
+
225
+ const markdownComponents: Components = {
226
+ a({ children, href }) {
227
+ return (
228
+ <a className="font-semibold" href={href}>
229
+ {children}
230
+ </a>
231
+ );
232
+ },
233
+ blockquote({ children }) {
234
+ return <span className="block border-l-2 pl-4 italic">{children}</span>;
235
+ },
236
+ code({ children }) {
237
+ return <code className="rounded-md px-1.5 py-0.5 font-mono text-sm">{children}</code>;
238
+ },
239
+ h1({ children }) {
240
+ return <span className="block pt-4 text-3xl leading-tight font-medium tracking-tight">{children}</span>;
241
+ },
242
+ h2({ children }) {
243
+ return <span className="block pt-3 text-xl font-medium">{children}</span>;
244
+ },
245
+ h3({ children }) {
246
+ return <span className="block pt-2 text-lg font-medium">{children}</span>;
247
+ },
248
+ li({ children }) {
249
+ return <span className="block">{children}</span>;
250
+ },
251
+ ol({ children }) {
252
+ return <span className="block list-decimal space-y-2 pl-5">{children}</span>;
253
+ },
254
+ p({ children }) {
255
+ return <span className="block">{children}</span>;
256
+ },
257
+ pre({ children }) {
258
+ return <span className="block overflow-x-auto rounded-2xl p-4 font-mono text-sm text-slate-100">{children}</span>;
259
+ },
260
+ strong({ children }) {
261
+ return <strong className="font-bold">{children}</strong>;
262
+ },
263
+ ul({ children }) {
264
+ return <span className="block list-disc space-y-2 pl-5">{children}</span>;
265
+ },
266
+ };
267
+
268
+ function resolveCustomization(
269
+ context: TemplateRuntimeContextValue,
270
+ fieldId: number,
271
+ ) {
272
+ const key = String(fieldId);
273
+ return Object.hasOwn(context.values, key)
274
+ ? context.values[key]
275
+ : context.defaults[key];
276
+ }
277
+
278
+ function normalizeValues(value: unknown): CustomizationValues {
279
+ return normalizeCustomizationValues(value) ?? {};
280
+ }
@@ -0,0 +1,121 @@
1
+ import ts from "typescript";
2
+
3
+ export const TEMPLATE_CODE_MAX_BYTES = 250_000;
4
+
5
+ const ALLOWED_TEMPLATE_IMPORTS = new Set([
6
+ "@noweb",
7
+ "@react-three/drei",
8
+ "@react-three/fiber",
9
+ "@react-three/rapier",
10
+ "clsx",
11
+ "gsap",
12
+ "gsap/all",
13
+ "gsap/ScrollTrigger",
14
+ "lucide-react",
15
+ "motion/react",
16
+ "noweb",
17
+ "ogl",
18
+ "react",
19
+ "react-dom",
20
+ "react-dom/client",
21
+ "react-markdown",
22
+ "remark-gfm",
23
+ "shaders/react",
24
+ "three",
25
+ "three/examples/jsm/controls/OrbitControls.js",
26
+ "three/examples/jsm/loaders/GLTFLoader.js",
27
+ ]);
28
+
29
+ const BLOCKED_JSX_ELEMENTS = new Set(["form", "iframe", "script"]);
30
+ const BLOCKED_NETWORK_APIS = new Set([
31
+ "EventSource",
32
+ "WebSocket",
33
+ "XMLHttpRequest",
34
+ "fetch",
35
+ "sendBeacon",
36
+ ]);
37
+
38
+ export function validateTemplateCode(code: string) {
39
+ const size = Buffer.byteLength(code, "utf8");
40
+ if (size > TEMPLATE_CODE_MAX_BYTES) {
41
+ throw new Error(`Template code must be ${TEMPLATE_CODE_MAX_BYTES} bytes or smaller.`);
42
+ }
43
+
44
+ const source = ts.createSourceFile(
45
+ "template.tsx",
46
+ code,
47
+ ts.ScriptTarget.Latest,
48
+ true,
49
+ ts.ScriptKind.TSX,
50
+ );
51
+
52
+ for (const statement of source.statements) {
53
+ if (!ts.isImportDeclaration(statement)) continue;
54
+ const specifier = statement.moduleSpecifier;
55
+ if (!ts.isStringLiteral(specifier)) continue;
56
+
57
+ const moduleName = specifier.text;
58
+ if (!ALLOWED_TEMPLATE_IMPORTS.has(moduleName)) {
59
+ throw new Error(`Template import is not allowed: ${moduleName}`);
60
+ }
61
+ }
62
+
63
+
64
+ function inspect(node: ts.Node) {
65
+ if (ts.isJsxOpeningElement(node) || ts.isJsxSelfClosingElement(node)) {
66
+ const tagName = node.tagName.getText(source).toLowerCase();
67
+ if (BLOCKED_JSX_ELEMENTS.has(tagName)) {
68
+ throw new Error(`Template element is not allowed: <${tagName}>`);
69
+ }
70
+
71
+ for (const attribute of node.attributes.properties) {
72
+ if (
73
+ ts.isJsxAttribute(attribute) &&
74
+ attribute.name.getText(source) === "dangerouslySetInnerHTML"
75
+ ) {
76
+ throw new Error("dangerouslySetInnerHTML is not allowed in templates");
77
+ }
78
+ }
79
+ }
80
+
81
+ if (ts.isIdentifier(node) && BLOCKED_NETWORK_APIS.has(node.text)) {
82
+ throw new Error(`Template network API is not allowed: ${node.text}`);
83
+ }
84
+
85
+ if (
86
+ ts.isCallExpression(node) &&
87
+ ts.isIdentifier(node.expression) &&
88
+ node.expression.text === "custom" &&
89
+ node.arguments.length !== 1
90
+ ) {
91
+ throw new Error("custom() accepts exactly one argument");
92
+ }
93
+
94
+ if (
95
+ ts.isElementAccessExpression(node) &&
96
+ ts.isStringLiteralLike(node.argumentExpression) &&
97
+ BLOCKED_NETWORK_APIS.has(node.argumentExpression.text)
98
+ ) {
99
+ throw new Error(`Template network API is not allowed: ${node.argumentExpression.text}`);
100
+ }
101
+
102
+ if (
103
+ ts.isCallExpression(node) &&
104
+ ts.isPropertyAccessExpression(node.expression) &&
105
+ node.expression.name.text === "createElement" &&
106
+ (!node.arguments[0] ||
107
+ !ts.isStringLiteralLike(node.arguments[0]) ||
108
+ BLOCKED_JSX_ELEMENTS.has(node.arguments[0].text.toLowerCase()))
109
+ ) {
110
+ throw new Error("Dynamic or blocked element creation is not allowed");
111
+ }
112
+
113
+ ts.forEachChild(node, inspect);
114
+ }
115
+
116
+ inspect(source);
117
+
118
+ if (/\@import\s+(?:url\()?\s*["']?https?:\/\//i.test(code)) {
119
+ throw new Error("Remote CSS imports are not allowed in templates");
120
+ }
121
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,38 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2017",
4
+ "lib": ["dom", "dom.iterable", "esnext"],
5
+ "allowJs": true,
6
+ "allowImportingTsExtensions": true,
7
+ "skipLibCheck": true,
8
+ "strict": true,
9
+ "noEmit": true,
10
+ "esModuleInterop": true,
11
+ "module": "esnext",
12
+ "moduleResolution": "bundler",
13
+ "resolveJsonModule": true,
14
+ "isolatedModules": true,
15
+ "jsx": "react-jsx",
16
+ "incremental": true,
17
+ "plugins": [
18
+ {
19
+ "name": "next"
20
+ }
21
+ ],
22
+ "paths": {
23
+ "@/*": ["./*"],
24
+ "@noweb": ["./templates/templateRuntime.tsx"],
25
+ "noweb": ["./templates/templateRuntime.tsx"],
26
+ "@noweb/template-runtime": ["./templates/templateRuntime.tsx"]
27
+ }
28
+ },
29
+ "include": [
30
+ "next-env.d.ts",
31
+ "**/*.ts",
32
+ "**/*.tsx",
33
+ ".next/types/**/*.ts",
34
+ ".next/dev/types/**/*.ts",
35
+ "**/*.mts"
36
+ ],
37
+ "exclude": ["node_modules"]
38
+ }