Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
114 changes: 114 additions & 0 deletions packages/static/src/plugin/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,120 @@ const fsRoutesOptions: FunstackStaticOptions = {
},
};

/**
* Find a plugin by name in the flattened plugin list.
*/
function getPlugin(plugins: (Plugin | Plugin[])[], name: string): Plugin {
const plugin = plugins.flat().find((p): p is Plugin => p.name === name);
if (!plugin) {
throw new Error(`${name} plugin not found`);
}
return plugin;
}

/**
* Run `configResolved` (with the given Vite root) followed by `load(id)` on
* the virtual-entry plugin, returning the generated module code.
*/
function loadVirtualModule(
options: FunstackStaticOptions,
viteRoot: string,
id: string,
): string {
const plugins = funstackStatic(options);
const configPlugin = getPlugin(plugins, "@funstack/static:config");
const configResolved = configPlugin.configResolved;
const configResolvedHandler =
typeof configResolved === "function"
? configResolved
: configResolved?.handler;
if (!configResolvedHandler) {
throw new Error("configResolved hook not found");
}
// The hook only reads `config.root`.
configResolvedHandler.call({} as never, { root: viteRoot } as never);

const virtualPlugin = getPlugin(plugins, "@funstack/static:virtual-entry");
const load = virtualPlugin.load;
const loadHandler = typeof load === "function" ? load : load?.handler;
if (!loadHandler) {
throw new Error("load hook not found");
}
const code = loadHandler.call({} as never, id, undefined);
if (typeof code !== "string") {
throw new Error(`load did not return code for ${id}`);
}
return code;
}

describe("virtual module path escaping", () => {
// A Vite root whose path needs escaping when embedded in a JS string
// literal. Such paths must not produce syntax errors (or injected code) in
// the generated virtual modules (#149).
const trickyRoot = '/pro"ject';

it("escapes root and app paths in the single-entry entries module", () => {
const code = loadVirtualModule(
{ root: "./src/root.tsx", app: "./src/App.tsx" },
trickyRoot,
"\0virtual:funstack/entries",
);
expect(code).toContain('import Root from "/pro\\"ject/src/root.tsx";');
expect(code).toContain('import App from "/pro\\"ject/src/App.tsx";');
});

it("escapes the entries module path in multi-entry mode", () => {
const code = loadVirtualModule(
{ entries: "./src/entries.tsx" },
trickyRoot,
"\0virtual:funstack/entries",
);
expect(code).toBe('export { default } from "/pro\\"ject/src/entries.tsx";');
});

it("escapes root and adapter paths in the fsRoutes entries module", () => {
const code = loadVirtualModule(
{
fsRoutes: {
dir: "./src/pages",
root: "./src/root.tsx",
adapter: "./src/adapter.ts",
},
},
trickyRoot,
"\0virtual:funstack/entries",
);
expect(code).toContain('import Root from "/pro\\"ject/src/root.tsx";');
expect(code).toContain('import adapter from "/pro\\"ject/src/adapter.ts";');
});

it("escapes the clientInit path in the client-init module", () => {
const code = loadVirtualModule(
{
root: "./src/root.tsx",
app: "./src/App.tsx",
clientInit: "./src/init.ts",
},
trickyRoot,
"\0virtual:funstack/client-init",
);
expect(code).toBe('import "/pro\\"ject/src/init.ts";');
});

it("escapes the build entry path in the build-entry module", () => {
const code = loadVirtualModule(
{
root: "./src/root.tsx",
app: "./src/App.tsx",
build: "./src/build.ts",
},
trickyRoot,
"\0virtual:funstack/build-entry",
);
expect(code).toBe('export { default } from "/pro\\"ject/src/build.ts";');
});
});

describe("configEnvironment optimizeDeps", () => {
it("includes React and ReactDOM as a single (bare) optimized chunk", () => {
const { include, exclude } = runConfigEnvironment(
Expand Down
14 changes: 7 additions & 7 deletions packages/static/src/plugin/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -332,8 +332,8 @@ export default function funstackStatic(
const { root, adapter, globBase } = resolvedFsRoutes;
const globPattern = `${globBase}/**/*.{tsx,jsx}`;
const lines = [
`import Root from "${root}";`,
`import adapter from "${adapter}";`,
`import Root from ${JSON.stringify(root)};`,
`import adapter from ${JSON.stringify(adapter)};`,
`import { createFsRoutesEntries } from "@funstack/static/fs-routes";`,
`const modules = import.meta.glob(${JSON.stringify(globPattern)}, { eager: true });`,
`export default createFsRoutesEntries({`,
Expand All @@ -347,13 +347,13 @@ export default function funstackStatic(
}
if (isMultiEntry) {
// Re-export the user's entries module
return `export { default } from "${resolvedEntriesModule}";`;
return `export { default } from ${JSON.stringify(resolvedEntriesModule)};`;
}
// Synthesize a single-entry array from root+app
const { root, app } = JSON.parse(resolvedEntriesModule);
return [
`import Root from "${root}";`,
`import App from "${app}";`,
`import Root from ${JSON.stringify(root)};`,
`import App from ${JSON.stringify(app)};`,
`export default function getEntries() {`,
` return [{ path: "index.html", root: { default: Root }, app: { default: App } }];`,
`}`,
Expand All @@ -367,13 +367,13 @@ export default function funstackStatic(
}
if (id === "\0virtual:funstack/client-init") {
if (resolvedClientInitEntry) {
return `import "${resolvedClientInitEntry}";`;
return `import ${JSON.stringify(resolvedClientInitEntry)};`;
}
return "";
}
if (id === "\0virtual:funstack/build-entry") {
if (resolvedBuildEntry) {
return `export { default } from "${resolvedBuildEntry}";`;
return `export { default } from ${JSON.stringify(resolvedBuildEntry)};`;
}
return "export default undefined;";
}
Expand Down