vite-plugin-nora 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 +21 -0
- package/NOTICE +26 -0
- package/README.md +113 -0
- package/licenses/geist-OFL.txt +93 -0
- package/licenses/lucide-ISC.txt +17 -0
- package/package.json +90 -0
- package/src/cli.js +101 -0
- package/src/client/App.jsx +216 -0
- package/src/client/canvas/Canvas.jsx +95 -0
- package/src/client/canvas/ErrorBoundary.jsx +56 -0
- package/src/client/canvas/Preview.jsx +70 -0
- package/src/client/canvas/Viewport.jsx +211 -0
- package/src/client/chords.js +38 -0
- package/src/client/css.d.ts +5 -0
- package/src/client/fonts.css +48 -0
- package/src/client/frame.css +107 -0
- package/src/client/frame.html +11 -0
- package/src/client/frame.jsx +73 -0
- package/src/client/index.html +12 -0
- package/src/client/main.jsx +10 -0
- package/src/client/open-folder.js +84 -0
- package/src/client/selection.js +22 -0
- package/src/client/styles.css +1264 -0
- package/src/client/sweep/SweepPanel.jsx +206 -0
- package/src/client/sweep/measure.js +230 -0
- package/src/client/sweep/report.js +54 -0
- package/src/client/sweep/run-sweep.js +185 -0
- package/src/client/toolbar/Picker.jsx +343 -0
- package/src/client/toolbar/Toolbar.jsx +231 -0
- package/src/client/toolbar/ViewportMenu.jsx +83 -0
- package/src/client/toolbar/bar-shape.js +402 -0
- package/src/client/toolbar/icons.jsx +129 -0
- package/src/client/toolbar/use-draggable-bar.js +204 -0
- package/src/client/viewports.js +43 -0
- package/src/client/virtual.d.ts +28 -0
- package/src/index.js +4 -0
- package/src/server/create-server.js +147 -0
- package/src/server/plugin.js +207 -0
- package/src/server/safe-path.js +42 -0
- package/src/server/scan.js +350 -0
- package/types/index.d.ts +3 -0
- package/types/server/create-server.d.ts +22 -0
- package/types/server/plugin.d.ts +20 -0
- package/types/server/safe-path.d.ts +25 -0
- package/types/server/scan.d.ts +106 -0
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import fg from "fast-glob";
|
|
4
|
+
import { transform } from "esbuild";
|
|
5
|
+
import { init as initLexer, parse as lexParse } from "es-module-lexer";
|
|
6
|
+
|
|
7
|
+
const DEFAULT_INCLUDE = ["**/*.{tsx,jsx}"];
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Skipped unless a project says otherwise.
|
|
11
|
+
*
|
|
12
|
+
* The index entries are the ones worth knowing about. They are here because an
|
|
13
|
+
* `index.js` is usually a barrel that re-exports its neighbours, and listing it
|
|
14
|
+
* would show every component in the folder twice. But a project whose screens
|
|
15
|
+
* live in `index.jsx` — one folder per screen, the screen at its root — is a
|
|
16
|
+
* real convention, and under this default nora cannot see those screens at all.
|
|
17
|
+
*
|
|
18
|
+
* That is a configuration problem rather than a scanning one: set `include` and
|
|
19
|
+
* `exclude` in nora.config to drop the index lines, and the folder's own design
|
|
20
|
+
* becomes visible to `pickEntry` like anything else.
|
|
21
|
+
*/
|
|
22
|
+
const DEFAULT_EXCLUDE = [
|
|
23
|
+
"**/node_modules/**",
|
|
24
|
+
"**/dist/**",
|
|
25
|
+
"**/build/**",
|
|
26
|
+
"**/.next/**",
|
|
27
|
+
"**/*.test.*",
|
|
28
|
+
"**/*.spec.*",
|
|
29
|
+
"**/*.stories.*",
|
|
30
|
+
"**/index.ts",
|
|
31
|
+
"**/index.tsx",
|
|
32
|
+
"**/index.js",
|
|
33
|
+
"**/index.jsx",
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
/** Directories never worth showing in the folder picker. */
|
|
37
|
+
const HIDDEN_DIRS = new Set([
|
|
38
|
+
"node_modules",
|
|
39
|
+
".git",
|
|
40
|
+
"dist",
|
|
41
|
+
"build",
|
|
42
|
+
".next",
|
|
43
|
+
".cache",
|
|
44
|
+
".vite",
|
|
45
|
+
"coverage",
|
|
46
|
+
".turbo",
|
|
47
|
+
".svelte-kit",
|
|
48
|
+
]);
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* A component looks like an exported binding whose name starts uppercase.
|
|
52
|
+
* This is a heuristic and it will be wrong sometimes — which is why the client
|
|
53
|
+
* keeps a "show all exports" affordance rather than treating it as truth.
|
|
54
|
+
*/
|
|
55
|
+
const looksLikeComponent = (name) => /^[A-Z]/.test(name);
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Read a source file and return its export names, without executing it.
|
|
59
|
+
*
|
|
60
|
+
* es-module-lexer can't read TSX directly, so esbuild strips types first.
|
|
61
|
+
* esbuild ships with Vite, costs about a millisecond a file, and gets generics
|
|
62
|
+
* and decorators right — all things a regex over the source would not.
|
|
63
|
+
*
|
|
64
|
+
* @param {string} file absolute path
|
|
65
|
+
*/
|
|
66
|
+
async function readExports(file) {
|
|
67
|
+
const source = await fs.readFile(file, "utf8");
|
|
68
|
+
|
|
69
|
+
// Directive detection has to happen on the raw source: esbuild strips
|
|
70
|
+
// "use server" during transform, and an RSC cannot render in our canvas.
|
|
71
|
+
const head = source.slice(0, 400);
|
|
72
|
+
const isServerOnly = /^\s*["']use server["']/m.test(head);
|
|
73
|
+
|
|
74
|
+
const loader = file.endsWith(".tsx") ? "tsx" : file.endsWith(".ts") ? "ts" : "jsx";
|
|
75
|
+
|
|
76
|
+
let code;
|
|
77
|
+
try {
|
|
78
|
+
({ code } = await transform(source, { loader, format: "esm" }));
|
|
79
|
+
} catch (err) {
|
|
80
|
+
return { exports: [], isServerOnly, error: String(err.message ?? err) };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
await initLexer;
|
|
84
|
+
try {
|
|
85
|
+
// The lexer returns imports first. They used to be discarded; they are what
|
|
86
|
+
// lets `pickEntry` tell a composition root from a leaf, and they cost
|
|
87
|
+
// nothing because the parse happens anyway.
|
|
88
|
+
const [imp, exp] = lexParse(code, file);
|
|
89
|
+
return {
|
|
90
|
+
// The lexer's Export/Import unions include variants with no `n` (a
|
|
91
|
+
// `export * from` re-export, a dynamic import with a computed specifier).
|
|
92
|
+
// Both come back undefined rather than throwing, which the filter below
|
|
93
|
+
// and `looksLikeComponent` downstream already handle.
|
|
94
|
+
exports: exp.map((e) => /** @type {{ n?: string }} */ (e).n).filter(Boolean),
|
|
95
|
+
imports: imp.map((i) => /** @type {{ n?: string }} */ (i).n).filter(Boolean),
|
|
96
|
+
isServerOnly,
|
|
97
|
+
error: null,
|
|
98
|
+
};
|
|
99
|
+
} catch (err) {
|
|
100
|
+
return { exports: [], imports: [], isServerOnly, error: String(err.message ?? err) };
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Resolve a relative import onto the set of files actually scanned.
|
|
106
|
+
*
|
|
107
|
+
* Only relative specifiers are followed. A bare or aliased one ("react",
|
|
108
|
+
* "@ui/tokens.js") cannot be resolved without the project's resolver config,
|
|
109
|
+
* and for this purpose it does not need to be: the question is only ever
|
|
110
|
+
* "does this file pull in a sibling of its own", and a sibling is relative.
|
|
111
|
+
*/
|
|
112
|
+
function resolveSibling(file, spec, files) {
|
|
113
|
+
const parts = file.split("/").slice(0, -1);
|
|
114
|
+
for (const piece of spec.split("/")) {
|
|
115
|
+
if (piece === "" || piece === ".") continue;
|
|
116
|
+
if (piece === "..") parts.pop();
|
|
117
|
+
else parts.push(piece);
|
|
118
|
+
}
|
|
119
|
+
const joined = parts.join("/");
|
|
120
|
+
if (files.has(joined)) return joined;
|
|
121
|
+
for (const ext of [".jsx", ".tsx", ".js", ".ts"]) {
|
|
122
|
+
if (files.has(joined + ext)) return joined + ext;
|
|
123
|
+
}
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const normalise = (s) => s.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Which of a folder's entries *is* the folder — the design the rest compose into.
|
|
131
|
+
*
|
|
132
|
+
* The question this answers is "you pointed nora at a project folder; what did
|
|
133
|
+
* you want to look at". A folder of a dozen components usually has one that is
|
|
134
|
+
* the actual screen and eleven that are parts of it, and landing on the screen
|
|
135
|
+
* is the difference between the tool opening on your work and opening on a list.
|
|
136
|
+
*
|
|
137
|
+
* The load-bearing rule is the second one. A composition root is a file that
|
|
138
|
+
* imports siblings and that no sibling imports — which is close to a definition
|
|
139
|
+
* of "the top of this folder" rather than a guess about naming, and it is why
|
|
140
|
+
* the imports above are collected at all. Ties break toward the file that pulls
|
|
141
|
+
* in the most, since that is the fullest assembly of the folder.
|
|
142
|
+
*
|
|
143
|
+
* Everything around it is scaffolding: an explicit config entry wins outright
|
|
144
|
+
* because the author said so, a name matching the folder catches the flat case
|
|
145
|
+
* where nothing imports anything, and first-alphabetically keeps the result
|
|
146
|
+
* deterministic rather than dependent on filesystem order.
|
|
147
|
+
*
|
|
148
|
+
* Pure on purpose — it takes plain data, so it is testable without a
|
|
149
|
+
* filesystem, a bundler, or a platform-matched esbuild.
|
|
150
|
+
*
|
|
151
|
+
* @param {object} opts
|
|
152
|
+
* @param {Array} opts.entries scanned entries, in file order
|
|
153
|
+
* @param {Map<string,string[]>} opts.imports rel file -> its import specifiers
|
|
154
|
+
* @param {string} [opts.folder] the folder's own name, for the naming rule
|
|
155
|
+
* @param {string} [opts.override] a name/id/file from nora.config
|
|
156
|
+
* @returns {string|null} the chosen entry id
|
|
157
|
+
*/
|
|
158
|
+
export function pickEntry({ entries, imports, folder, override }) {
|
|
159
|
+
const live = (entries ?? []).filter((e) => !e.unsupported);
|
|
160
|
+
if (!live.length) return null;
|
|
161
|
+
|
|
162
|
+
if (override) {
|
|
163
|
+
const hit = live.find((e) => e.name === override || e.id === override || e.file === override);
|
|
164
|
+
if (hit) return hit.id;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const files = new Set(live.map((e) => e.file));
|
|
168
|
+
const pulls = new Map();
|
|
169
|
+
const pulledBy = new Map();
|
|
170
|
+
|
|
171
|
+
for (const [file, specs] of imports ?? []) {
|
|
172
|
+
if (!files.has(file)) continue;
|
|
173
|
+
for (const spec of specs) {
|
|
174
|
+
if (!spec.startsWith(".")) continue;
|
|
175
|
+
const target = resolveSibling(file, spec, files);
|
|
176
|
+
if (!target || target === file) continue;
|
|
177
|
+
pulls.set(file, (pulls.get(file) ?? 0) + 1);
|
|
178
|
+
pulledBy.set(target, (pulledBy.get(target) ?? 0) + 1);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const roots = live.filter(
|
|
183
|
+
(e) => (pulls.get(e.file) ?? 0) > 0 && (pulledBy.get(e.file) ?? 0) === 0,
|
|
184
|
+
);
|
|
185
|
+
if (roots.length) {
|
|
186
|
+
roots.sort(
|
|
187
|
+
(a, b) => (pulls.get(b.file) ?? 0) - (pulls.get(a.file) ?? 0) || a.file.localeCompare(b.file),
|
|
188
|
+
);
|
|
189
|
+
return roots[0].id;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (folder) {
|
|
193
|
+
const want = normalise(folder);
|
|
194
|
+
const named =
|
|
195
|
+
live.find((e) => normalise(e.name) === want) ??
|
|
196
|
+
live.find((e) => normalise(e.name).startsWith(want));
|
|
197
|
+
if (named) return named.id;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return live[0].id;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Turn a directory into a flat list of registry entries — one per exported
|
|
205
|
+
* component, not one per file — plus which of them is the folder's own design.
|
|
206
|
+
*
|
|
207
|
+
* @param {object} opts
|
|
208
|
+
* @param {string} opts.root project root (all paths are reported relative to it)
|
|
209
|
+
* @param {string} opts.dir absolute directory to scan
|
|
210
|
+
* @param {string[]} [opts.include]
|
|
211
|
+
* @param {string[]} [opts.exclude]
|
|
212
|
+
* @param {string} [opts.entry] nora.config override for this folder
|
|
213
|
+
* @returns {Promise<{entries: Array, entryId: string|null}>}
|
|
214
|
+
*/
|
|
215
|
+
export async function scanDirectory({ root, dir, include, exclude, entry }) {
|
|
216
|
+
const files = await fg(include?.length ? include : DEFAULT_INCLUDE, {
|
|
217
|
+
cwd: dir,
|
|
218
|
+
absolute: true,
|
|
219
|
+
ignore: [...DEFAULT_EXCLUDE, ...(exclude ?? [])],
|
|
220
|
+
onlyFiles: true,
|
|
221
|
+
suppressErrors: true,
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
files.sort();
|
|
225
|
+
|
|
226
|
+
const entries = [];
|
|
227
|
+
const imports = new Map();
|
|
228
|
+
|
|
229
|
+
for (const file of files) {
|
|
230
|
+
const { exports, imports: fileImports, isServerOnly, error } = await readExports(file);
|
|
231
|
+
const relFile = path.relative(root, file).split(path.sep).join("/");
|
|
232
|
+
const url = "/" + relFile;
|
|
233
|
+
const base = path.basename(file).replace(/\.(tsx|jsx|ts|js)$/, "");
|
|
234
|
+
const group = path.relative(dir, path.dirname(file)).split(path.sep).filter(Boolean).join("/");
|
|
235
|
+
|
|
236
|
+
if (error) {
|
|
237
|
+
entries.push({
|
|
238
|
+
id: `${relFile}#__error`,
|
|
239
|
+
name: base,
|
|
240
|
+
file: relFile,
|
|
241
|
+
url,
|
|
242
|
+
exportName: "default",
|
|
243
|
+
group,
|
|
244
|
+
unsupported: `Could not parse: ${error}`,
|
|
245
|
+
});
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
imports.set(relFile, fileImports ?? []);
|
|
250
|
+
|
|
251
|
+
for (const exportName of exports) {
|
|
252
|
+
const name = exportName === "default" ? base : exportName;
|
|
253
|
+
if (!looksLikeComponent(name)) continue;
|
|
254
|
+
|
|
255
|
+
entries.push({
|
|
256
|
+
id: `${relFile}#${exportName}`,
|
|
257
|
+
name,
|
|
258
|
+
file: relFile,
|
|
259
|
+
url,
|
|
260
|
+
exportName,
|
|
261
|
+
group,
|
|
262
|
+
unsupported: isServerOnly
|
|
263
|
+
? 'This file is marked "use server" and cannot render in a client canvas.'
|
|
264
|
+
: null,
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
return {
|
|
270
|
+
entries,
|
|
271
|
+
entryId: pickEntry({
|
|
272
|
+
entries,
|
|
273
|
+
imports,
|
|
274
|
+
folder: path.basename(dir),
|
|
275
|
+
override: entry,
|
|
276
|
+
}),
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* The half of an entry that crosses into the browser.
|
|
282
|
+
*
|
|
283
|
+
* `plugin.js` used to rebuild this object by hand while generating the registry
|
|
284
|
+
* module, which meant the shape existed in two places and adding a field here
|
|
285
|
+
* reached the client as `undefined`. It is written once, here, next to the code
|
|
286
|
+
* that builds the entry in the first place.
|
|
287
|
+
*
|
|
288
|
+
* `url` is deliberately absent: it is the server's import specifier, consumed
|
|
289
|
+
* while generating the module and meaningless to the client afterwards.
|
|
290
|
+
*
|
|
291
|
+
* @typedef {object} ScannedEntry
|
|
292
|
+
* @property {string} id
|
|
293
|
+
* @property {string} name
|
|
294
|
+
* @property {string} file
|
|
295
|
+
* @property {string} url import specifier, server-side only
|
|
296
|
+
* @property {string} exportName
|
|
297
|
+
* @property {string} group
|
|
298
|
+
* @property {string | null} [unsupported]
|
|
299
|
+
*
|
|
300
|
+
* @param {ScannedEntry} e a scanned entry
|
|
301
|
+
*/
|
|
302
|
+
export function clientEntry(e) {
|
|
303
|
+
return {
|
|
304
|
+
id: e.id,
|
|
305
|
+
name: e.name,
|
|
306
|
+
file: e.file,
|
|
307
|
+
exportName: e.exportName,
|
|
308
|
+
group: e.group,
|
|
309
|
+
unsupported: e.unsupported ?? null,
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* List immediate subdirectories of `dir`, each with a count of candidate
|
|
315
|
+
* component files beneath it, so the picker can show "24" next to a folder.
|
|
316
|
+
*
|
|
317
|
+
* @param {string} dir absolute directory
|
|
318
|
+
*/
|
|
319
|
+
export async function listDirectories(dir) {
|
|
320
|
+
let dirents;
|
|
321
|
+
try {
|
|
322
|
+
dirents = await fs.readdir(dir, { withFileTypes: true });
|
|
323
|
+
} catch {
|
|
324
|
+
return [];
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
const dirs = dirents
|
|
328
|
+
.filter((d) => d.isDirectory() && !HIDDEN_DIRS.has(d.name) && !d.name.startsWith("."))
|
|
329
|
+
.map((d) => d.name)
|
|
330
|
+
.sort();
|
|
331
|
+
|
|
332
|
+
return Promise.all(
|
|
333
|
+
dirs.map(async (name) => {
|
|
334
|
+
const full = path.join(dir, name);
|
|
335
|
+
let count = 0;
|
|
336
|
+
try {
|
|
337
|
+
const found = await fg(DEFAULT_INCLUDE, {
|
|
338
|
+
cwd: full,
|
|
339
|
+
ignore: DEFAULT_EXCLUDE,
|
|
340
|
+
onlyFiles: true,
|
|
341
|
+
suppressErrors: true,
|
|
342
|
+
});
|
|
343
|
+
count = found.length;
|
|
344
|
+
} catch {
|
|
345
|
+
/* unreadable directory — report it with a zero count rather than hiding it */
|
|
346
|
+
}
|
|
347
|
+
return { name, count };
|
|
348
|
+
}),
|
|
349
|
+
);
|
|
350
|
+
}
|
package/types/index.d.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/** Walk up from `start` looking for a package.json to treat as the project root. */
|
|
2
|
+
export declare function findProjectRoot(start: any): string;
|
|
3
|
+
/**
|
|
4
|
+
* Boot a Vite dev server rooted in the user's project, inheriting their config,
|
|
5
|
+
* and serve our client through it.
|
|
6
|
+
*
|
|
7
|
+
* @param {object} opts
|
|
8
|
+
* @param {string} opts.root
|
|
9
|
+
* @param {number} [opts.port]
|
|
10
|
+
* @param {string|null} [opts.dir] relative folder to open on boot
|
|
11
|
+
*/
|
|
12
|
+
export declare function createPreviewServer({ root, port, dir }: {
|
|
13
|
+
root: string;
|
|
14
|
+
port?: number;
|
|
15
|
+
dir?: string | null;
|
|
16
|
+
}): Promise<{
|
|
17
|
+
server: import("vite").ViteDevServer;
|
|
18
|
+
url: string;
|
|
19
|
+
inheritedConfig: boolean;
|
|
20
|
+
configPath: string;
|
|
21
|
+
addedReactPlugin: boolean;
|
|
22
|
+
}>;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
declare const VIRTUAL_ID = "virtual:nora/registry";
|
|
2
|
+
declare const RESOLVED_ID: string;
|
|
3
|
+
/**
|
|
4
|
+
* The Vite plugin: owns the virtual registry module and the /__nora/* control
|
|
5
|
+
* endpoints the toolbar talks to.
|
|
6
|
+
*
|
|
7
|
+
* @param {object} opts
|
|
8
|
+
* @param {string} opts.root absolute project root
|
|
9
|
+
* @param {string} [opts.initialDir] relative dir to open on boot
|
|
10
|
+
*/
|
|
11
|
+
export declare function nora({ root, initialDir }: {
|
|
12
|
+
root: string;
|
|
13
|
+
initialDir?: string;
|
|
14
|
+
}): {
|
|
15
|
+
name: string;
|
|
16
|
+
resolveId(id: any): string;
|
|
17
|
+
load(id: any): Promise<string>;
|
|
18
|
+
configureServer(server: any): void;
|
|
19
|
+
};
|
|
20
|
+
export { VIRTUAL_ID, RESOLVED_ID };
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve a caller-supplied path against the project root and refuse anything
|
|
3
|
+
* that escapes it.
|
|
4
|
+
*
|
|
5
|
+
* Both `/__nora/dirs` and `/__nora/scan` take a path straight off an HTTP request
|
|
6
|
+
* and hand it to `fs`. Without this clamp, `?path=../../../../etc` walks the
|
|
7
|
+
* whole disk of whoever is running the CLI.
|
|
8
|
+
*
|
|
9
|
+
* @param {string} root absolute project root
|
|
10
|
+
* @param {string} [p] untrusted relative path
|
|
11
|
+
* @returns {string} absolute path guaranteed to sit inside root
|
|
12
|
+
*/
|
|
13
|
+
export declare function safeResolve(root: string, p?: string): string;
|
|
14
|
+
/**
|
|
15
|
+
* Reject requests whose Host header is not a loopback address.
|
|
16
|
+
*
|
|
17
|
+
* A dev server bound to localhost is still reachable from any page in the
|
|
18
|
+
* user's browser via DNS rebinding — an attacker's site resolves their domain
|
|
19
|
+
* to 127.0.0.1 and then talks to us with their own Host header. Vite has
|
|
20
|
+
* shipped advisories for exactly this shape of bug.
|
|
21
|
+
*
|
|
22
|
+
* @param {import('node:http').IncomingMessage} req
|
|
23
|
+
* @returns {boolean}
|
|
24
|
+
*/
|
|
25
|
+
export declare function isLoopbackHost(req: import('node:http').IncomingMessage): boolean;
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which of a folder's entries *is* the folder — the design the rest compose into.
|
|
3
|
+
*
|
|
4
|
+
* The question this answers is "you pointed nora at a project folder; what did
|
|
5
|
+
* you want to look at". A folder of a dozen components usually has one that is
|
|
6
|
+
* the actual screen and eleven that are parts of it, and landing on the screen
|
|
7
|
+
* is the difference between the tool opening on your work and opening on a list.
|
|
8
|
+
*
|
|
9
|
+
* The load-bearing rule is the second one. A composition root is a file that
|
|
10
|
+
* imports siblings and that no sibling imports — which is close to a definition
|
|
11
|
+
* of "the top of this folder" rather than a guess about naming, and it is why
|
|
12
|
+
* the imports above are collected at all. Ties break toward the file that pulls
|
|
13
|
+
* in the most, since that is the fullest assembly of the folder.
|
|
14
|
+
*
|
|
15
|
+
* Everything around it is scaffolding: an explicit config entry wins outright
|
|
16
|
+
* because the author said so, a name matching the folder catches the flat case
|
|
17
|
+
* where nothing imports anything, and first-alphabetically keeps the result
|
|
18
|
+
* deterministic rather than dependent on filesystem order.
|
|
19
|
+
*
|
|
20
|
+
* Pure on purpose — it takes plain data, so it is testable without a
|
|
21
|
+
* filesystem, a bundler, or a platform-matched esbuild.
|
|
22
|
+
*
|
|
23
|
+
* @param {object} opts
|
|
24
|
+
* @param {Array} opts.entries scanned entries, in file order
|
|
25
|
+
* @param {Map<string,string[]>} opts.imports rel file -> its import specifiers
|
|
26
|
+
* @param {string} [opts.folder] the folder's own name, for the naming rule
|
|
27
|
+
* @param {string} [opts.override] a name/id/file from nora.config
|
|
28
|
+
* @returns {string|null} the chosen entry id
|
|
29
|
+
*/
|
|
30
|
+
export declare function pickEntry({ entries, imports, folder, override }: {
|
|
31
|
+
entries: any[];
|
|
32
|
+
imports: Map<string, string[]>;
|
|
33
|
+
folder?: string;
|
|
34
|
+
override?: string;
|
|
35
|
+
}): string | null;
|
|
36
|
+
/**
|
|
37
|
+
* Turn a directory into a flat list of registry entries — one per exported
|
|
38
|
+
* component, not one per file — plus which of them is the folder's own design.
|
|
39
|
+
*
|
|
40
|
+
* @param {object} opts
|
|
41
|
+
* @param {string} opts.root project root (all paths are reported relative to it)
|
|
42
|
+
* @param {string} opts.dir absolute directory to scan
|
|
43
|
+
* @param {string[]} [opts.include]
|
|
44
|
+
* @param {string[]} [opts.exclude]
|
|
45
|
+
* @param {string} [opts.entry] nora.config override for this folder
|
|
46
|
+
* @returns {Promise<{entries: Array, entryId: string|null}>}
|
|
47
|
+
*/
|
|
48
|
+
export declare function scanDirectory({ root, dir, include, exclude, entry }: {
|
|
49
|
+
root: string;
|
|
50
|
+
dir: string;
|
|
51
|
+
include?: string[];
|
|
52
|
+
exclude?: string[];
|
|
53
|
+
entry?: string;
|
|
54
|
+
}): Promise<{
|
|
55
|
+
entries: any[];
|
|
56
|
+
entryId: string | null;
|
|
57
|
+
}>;
|
|
58
|
+
export type ScannedEntry = {
|
|
59
|
+
id: string;
|
|
60
|
+
name: string;
|
|
61
|
+
file: string;
|
|
62
|
+
/**
|
|
63
|
+
* import specifier, server-side only
|
|
64
|
+
*/
|
|
65
|
+
url: string;
|
|
66
|
+
exportName: string;
|
|
67
|
+
group: string;
|
|
68
|
+
unsupported?: string | null;
|
|
69
|
+
};
|
|
70
|
+
/**
|
|
71
|
+
* The half of an entry that crosses into the browser.
|
|
72
|
+
*
|
|
73
|
+
* `plugin.js` used to rebuild this object by hand while generating the registry
|
|
74
|
+
* module, which meant the shape existed in two places and adding a field here
|
|
75
|
+
* reached the client as `undefined`. It is written once, here, next to the code
|
|
76
|
+
* that builds the entry in the first place.
|
|
77
|
+
*
|
|
78
|
+
* `url` is deliberately absent: it is the server's import specifier, consumed
|
|
79
|
+
* while generating the module and meaningless to the client afterwards.
|
|
80
|
+
*
|
|
81
|
+
* @typedef {object} ScannedEntry
|
|
82
|
+
* @property {string} id
|
|
83
|
+
* @property {string} name
|
|
84
|
+
* @property {string} file
|
|
85
|
+
* @property {string} url import specifier, server-side only
|
|
86
|
+
* @property {string} exportName
|
|
87
|
+
* @property {string} group
|
|
88
|
+
* @property {string | null} [unsupported]
|
|
89
|
+
*
|
|
90
|
+
* @param {ScannedEntry} e a scanned entry
|
|
91
|
+
*/
|
|
92
|
+
export declare function clientEntry(e: ScannedEntry): {
|
|
93
|
+
id: string;
|
|
94
|
+
name: string;
|
|
95
|
+
file: string;
|
|
96
|
+
exportName: string;
|
|
97
|
+
group: string;
|
|
98
|
+
unsupported: string;
|
|
99
|
+
};
|
|
100
|
+
/**
|
|
101
|
+
* List immediate subdirectories of `dir`, each with a count of candidate
|
|
102
|
+
* component files beneath it, so the picker can show "24" next to a folder.
|
|
103
|
+
*
|
|
104
|
+
* @param {string} dir absolute directory
|
|
105
|
+
*/
|
|
106
|
+
export declare function listDirectories(dir: string): Promise<any[]>;
|