@dogsbay/import 0.0.0-stage → 0.2.0-beta.163

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/README.md CHANGED
@@ -1,3 +1,20 @@
1
- # Temporary Holding Version
1
+ # @dogsbay/import
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ A site made with GitBook, VitePress, Docusaurus or Starlight, as a Dogsbay project: the one
4
+ import the playground and `dogsbay import` run. No Node.
5
+
6
+ ```ts
7
+ import { importSite } from "@dogsbay/import";
8
+
9
+ const { files, ledger, unread } = await importSite({
10
+ format: "starlight",
11
+ files, // Map<path, string | Uint8Array>: what you have read so far
12
+ paths, // every path of the site
13
+ more: async (want) => { /* read `want` into `files` */ },
14
+ choice: { languages: ["en", "fr"] },
15
+ siteName: "My docs",
16
+ });
17
+ ```
18
+
19
+ `siteChoices(format, files, paths, more)` lists what the user can choose. See
20
+ docs/import.md and docs-dev/import-command.md in the dogsbay repo.
@@ -0,0 +1,164 @@
1
+ /**
2
+ * `@dogsbay/import`: a site made with another tool, as a Dogsbay project (plans/import-command.md).
3
+ *
4
+ * The ONE import every host runs: the playground (files from a zip or GitHub) and `dogsbay import`
5
+ * (files from disk). It calls the format's browser entry (`@dogsbay/format-*\/migrate`) with the
6
+ * choices the user made, and in rounds asks the host for what the conversion read and was not
7
+ * given, then for the assets it copies. A host supplies only what is its own: reading files, the
8
+ * Docusaurus adapter object, Starlight's TypeScript-to-JavaScript converter.
9
+ *
10
+ * No Node, no network, nothing from the repo is run. Each format's conversion is loaded only when
11
+ * a site of that format is imported.
12
+ */
13
+ import type { ImportLedgerEntry } from "@dogsbay/types";
14
+ import type { DocusaurusAdapter } from "@dogsbay/format-docusaurus/adapter";
15
+ /** The formats this import converts. */
16
+ export declare const IMPORT_FORMATS: readonly ["gitbook", "vitepress", "docusaurus", "starlight"];
17
+ export type ImportFormat = (typeof IMPORT_FORMATS)[number];
18
+ export declare const isImportFormat: (f: string) => f is ImportFormat;
19
+ export type SiteFiles = Map<string, string | Uint8Array>;
20
+ /** Docusaurus products and versions ticked: per docs instance, its versions by name (none for an unversioned one). */
21
+ export interface Selection {
22
+ instance: string;
23
+ versions?: string[];
24
+ }
25
+ /** What the user chose for the site. Each field applies to the formats it names. */
26
+ export interface ImportChoice {
27
+ /** GitBook: one space of a multi-space repo; all when omitted. */
28
+ space?: string;
29
+ /** Docusaurus: a docs instance (its id); the config's only or default one when omitted. */
30
+ instance?: string;
31
+ /** Docusaurus: a version (versions.json); the current docs folder when omitted. */
32
+ version?: string;
33
+ /** Docusaurus: products and versions converted together (`migrateSelection`), in place of `instance` and `version`. */
34
+ select?: Selection[];
35
+ /** Docusaurus: an API viewer page split into API pages mounted at it (the default), or the whole API kept on its page. */
36
+ openapi?: "split" | "embed";
37
+ /** VitePress and Starlight: the languages kept (ids, the site's order; the first the default); every language when omitted. */
38
+ languages?: string[];
39
+ /** Starlight: the products kept (folders under `src/content/docs/`); the whole site when omitted or empty. */
40
+ products?: string[];
41
+ /** Starlight: a site adapter by name (`cloudflare`). Docusaurus takes the adapter object (`docusaurusAdapter`). */
42
+ adapter?: string;
43
+ }
44
+ export interface ImportSiteInput {
45
+ format: ImportFormat;
46
+ /** The site's files given so far, paths from the site's folder (a detection's `root`). */
47
+ files: SiteFiles;
48
+ /** Every path of the site, when the host lists more than it gave: what the conversion needs from the rest it asks for (`more`). */
49
+ paths?: Iterable<string>;
50
+ choice?: ImportChoice;
51
+ siteName?: string;
52
+ siteUrl?: string;
53
+ /** Where the site came from, for MIGRATION.md: the repo's URL, or its folder. */
54
+ sourceLabel?: string;
55
+ /** Docusaurus: how many products the site has; a selection from a site of several gets a `products:` block, so another can be added later. */
56
+ productCount?: number;
57
+ /** Docusaurus: the site adapter (`tigeraAdapter` …), loaded by the host. */
58
+ docusaurusAdapter?: DocusaurusAdapter;
59
+ /** Starlight with the Cloudflare adapter: `<TypeScriptExample>`'s JavaScript tab. */
60
+ typescriptToJs?: (ts: string) => Promise<string>;
61
+ /**
62
+ * Adds these site paths to `files` (those it can read): what the conversion read and was not
63
+ * given (`assets` false: it converts again), or the assets it copies (`assets` true). Without it
64
+ * the conversion runs once over what was given.
65
+ */
66
+ more?(paths: string[], assets: boolean): Promise<void>;
67
+ /** Runs again with the files asked for, at most, after the first (each round can find one more level of an include or `require` chain). Default 5, as the playground. */
68
+ rounds?: number;
69
+ }
70
+ export interface ImportSiteResult {
71
+ /** The Dogsbay project: `dogsbay.config.yml`, `content/**`, `MIGRATION.md` (and `data/**` when the site has data). */
72
+ files: SiteFiles;
73
+ ledger: ImportLedgerEntry[];
74
+ /** Site paths the conversion needed and was not given (the host could not read them, or found after the last round). */
75
+ unread: string[];
76
+ }
77
+ export declare const DEFAULT_ROUNDS = 5;
78
+ /**
79
+ * The site as a Dogsbay project: the format's conversion with the user's choices, run again with
80
+ * the files it asks for while there are any (`rounds` at most), then the assets it copies placed
81
+ * where it says (`copyTo`) without converting again. Files the host could not give are left out of
82
+ * `paths`, so the conversion goes on without them, and are returned in `unread`.
83
+ */
84
+ export declare function importSite(input: ImportSiteInput): Promise<ImportSiteResult>;
85
+ /** A language the site has, to choose. */
86
+ export interface LanguageOption {
87
+ id: string;
88
+ label: string;
89
+ isDefault: boolean;
90
+ pages: number;
91
+ }
92
+ /** A Starlight product (a folder of its docs), to choose. */
93
+ export interface ProductOption {
94
+ id: string;
95
+ label: string;
96
+ pages: number;
97
+ }
98
+ /** A Docusaurus product (docs instance) and its versions, to choose. */
99
+ export interface DocsProductOption {
100
+ name: string;
101
+ instance: string;
102
+ label: string;
103
+ versions: {
104
+ id: string;
105
+ name: string;
106
+ label?: string;
107
+ isDefault: boolean;
108
+ prerelease?: true;
109
+ }[];
110
+ }
111
+ /** What the user can choose for a site; an empty list is nothing to choose. */
112
+ export interface SiteChoices {
113
+ /** GitBook: the spaces of a multi-space repo (two or more). */
114
+ spaces: string[];
115
+ /** VitePress, Starlight: the languages (two or more). */
116
+ languages: LanguageOption[];
117
+ /** Starlight: its products, when it has no languages. */
118
+ products: ProductOption[];
119
+ /** Docusaurus: its products and their versions. */
120
+ docs: DocsProductOption[];
121
+ /** Files the choices needed and were not given. */
122
+ unread: string[];
123
+ }
124
+ /**
125
+ * What the user can choose for the site (the playground's offer), from the paths and the config
126
+ * files: asked for in rounds (`more`), as `importSite` asks.
127
+ */
128
+ export declare function siteChoices(format: ImportFormat, files: SiteFiles, paths: Iterable<string>, more?: (paths: string[]) => Promise<void>): Promise<SiteChoices>;
129
+ /**
130
+ * The files a host gives up front, read as text: pages, config, data and scripts the conversions
131
+ * read. Anything else (images, fonts, PDFs …) is listed only, and given when asked for.
132
+ */
133
+ export declare const TEXT_FILE: RegExp;
134
+ /** Text a Dogsbay project holds (what the SSG and the playground read). */
135
+ export declare const PROJECT_TEXT_EXTENSIONS: readonly string[];
136
+ /** Images a Dogsbay project holds, as bytes. */
137
+ export declare const PROJECT_IMAGE_EXTENSIONS: readonly string[];
138
+ export declare const isProjectText: (path: string) => boolean;
139
+ export declare const isProjectImage: (path: string) => boolean;
140
+ /**
141
+ * The files of an import's result a project keeps, as every host keeps them: text as text, images
142
+ * as bytes; anything else (a PDF, a font) is left out. The playground's store holds only these, so
143
+ * a host that wrote more would give a different project.
144
+ */
145
+ export declare function projectFiles(files: SiteFiles): SiteFiles;
146
+ /**
147
+ * A site's build output and caches: never given up front (a host lists them; a conversion that
148
+ * asks for one still gets it). Generated bundles there can be hundreds of MB.
149
+ */
150
+ export declare const BUILD_OUTPUT: RegExp;
151
+ /** The TypeScript examples converted in one import, at most (characters): Cloudflare's whole site has about 2 MB of them. */
152
+ export declare const EXAMPLES_BYTES: number;
153
+ /**
154
+ * Starlight's `typescriptToJs`, each example converted once whatever the round (an import runs the
155
+ * conversion again each round), and at most `maxChars` of examples in all: past it an example keeps
156
+ * its TypeScript only (the conversion takes a failed one so, and says so). A repo's examples are its
157
+ * own text: a hostile one must not keep the host formatting for minutes.
158
+ */
159
+ export declare function cappedTypescriptToJs(convert: (ts: string) => Promise<string>, maxChars?: number): (ts: string) => Promise<string>;
160
+ /**
161
+ * The Starlight adapter whose site this is (paths from the site's folder, `read` its text): Cloudflare's,
162
+ * when an `astro.config.*` names developers.cloudflare.com as its `site`. As the playground offers it.
163
+ */
164
+ export declare function starlightAdapterFor(paths: Iterable<string>, read: (path: string) => string | undefined): string | undefined;
package/dist/index.js ADDED
@@ -0,0 +1,204 @@
1
+ import { gitbookSpaces } from "@dogsbay/detect";
2
+ /** The formats this import converts. */
3
+ export const IMPORT_FORMATS = ["gitbook", "vitepress", "docusaurus", "starlight"];
4
+ export const isImportFormat = (f) => IMPORT_FORMATS.includes(f);
5
+ export const DEFAULT_ROUNDS = 5;
6
+ /**
7
+ * The site as a Dogsbay project: the format's conversion with the user's choices, run again with
8
+ * the files it asks for while there are any (`rounds` at most), then the assets it copies placed
9
+ * where it says (`copyTo`) without converting again. Files the host could not give are left out of
10
+ * `paths`, so the conversion goes on without them, and are returned in `unread`.
11
+ */
12
+ export async function importSite(input) {
13
+ const run = await runner(input);
14
+ const all = input.paths ? [...input.paths] : undefined;
15
+ const gone = new Set();
16
+ const paths = () => all?.filter((p) => !gone.has(p));
17
+ const rounds = Math.max(0, input.rounds ?? DEFAULT_ROUNDS);
18
+ let result = await run(paths());
19
+ for (let round = 0; round < rounds && result.unfetched.read.length && input.more; round++) {
20
+ const wanted = result.unfetched.read;
21
+ await input.more(wanted, false);
22
+ const missing = wanted.filter((p) => !input.files.has(p) && !gone.has(p));
23
+ for (const p of missing)
24
+ gone.add(p);
25
+ // Nothing new given and nothing newly left out: another run would ask the same
26
+ if (wanted.every((p) => !input.files.has(p)) && !missing.length)
27
+ break;
28
+ result = await run(paths());
29
+ }
30
+ const { copyTo } = result.unfetched;
31
+ if (copyTo.length && input.more) {
32
+ await input.more([...new Set(copyTo.map((c) => c.from))], true);
33
+ for (const { from, to } of copyTo) {
34
+ const data = input.files.get(from);
35
+ if (data !== undefined)
36
+ result.files.set(to, data);
37
+ }
38
+ }
39
+ const unread = [...new Set([...gone, ...result.unfetched.read])].sort();
40
+ return { files: result.files, ledger: result.ledger, unread };
41
+ }
42
+ /** The format's conversion with the input's choices, as a function of the paths listed. */
43
+ async function runner(input) {
44
+ const { files, choice = {}, siteName, siteUrl, sourceLabel } = input;
45
+ const common = { files, ...(siteName !== undefined ? { siteName } : {}), ...(siteUrl ? { siteUrl } : {}), ...(sourceLabel !== undefined ? { sourceLabel } : {}) };
46
+ const withPaths = (paths) => (paths ? { paths } : {});
47
+ switch (input.format) {
48
+ case "gitbook": {
49
+ const { migrate } = await import("@dogsbay/format-gitbook/migrate");
50
+ return (paths) => migrate({ ...common, ...withPaths(paths), ...(choice.space ? { choose: { space: choice.space } } : {}) });
51
+ }
52
+ case "vitepress": {
53
+ const { migrate } = await import("@dogsbay/format-vitepress/migrate");
54
+ return (paths) => migrate({ ...common, ...withPaths(paths), ...(choice.languages?.length ? { languages: choice.languages } : {}) });
55
+ }
56
+ case "docusaurus": {
57
+ const { migrate, migrateSelection } = await import("@dogsbay/format-docusaurus/migrate");
58
+ const shared = { ...(input.docusaurusAdapter ? { adapter: input.docusaurusAdapter } : {}), ...(choice.openapi ? { openapi: choice.openapi } : {}) };
59
+ const select = choice.select;
60
+ if (select?.length) {
61
+ // Several products in the site: a products: block even for one ticked, so another can be added later
62
+ const layout = input.productCount && input.productCount > 1 ? { layout: "products" } : {};
63
+ return (paths) => migrateSelection({ ...common, ...withPaths(paths), ...shared, select, ...layout });
64
+ }
65
+ const choose = choice.instance || choice.version ? { choose: { ...(choice.instance ? { instance: choice.instance } : {}), ...(choice.version ? { version: choice.version } : {}) } } : {};
66
+ return (paths) => migrate({ ...common, ...withPaths(paths), ...shared, ...choose });
67
+ }
68
+ case "starlight": {
69
+ const { migrate } = await import("@dogsbay/format-starlight/migrate");
70
+ // A site with languages is chosen by language, never by product too (the conversion refuses both)
71
+ const products = choice.products?.length ? { products: choice.products } : {};
72
+ const languages = !choice.products?.length && choice.languages?.length ? { languages: choice.languages } : {};
73
+ return (paths) => migrate({
74
+ ...common,
75
+ ...withPaths(paths),
76
+ ...products,
77
+ ...languages,
78
+ ...(choice.adapter ? { adapter: choice.adapter } : {}),
79
+ ...(input.typescriptToJs ? { typescriptToJs: input.typescriptToJs } : {}),
80
+ });
81
+ }
82
+ }
83
+ }
84
+ /**
85
+ * What the user can choose for the site (the playground's offer), from the paths and the config
86
+ * files: asked for in rounds (`more`), as `importSite` asks.
87
+ */
88
+ export async function siteChoices(format, files, paths, more) {
89
+ const all = [...paths];
90
+ const out = { spaces: [], languages: [], products: [], docs: [], unread: [] };
91
+ /** `choose` again with the files it asks for (`read`), while there are new ones. */
92
+ const rounds = async (choose) => {
93
+ let got = choose();
94
+ const tried = new Set();
95
+ for (let round = 0; round < DEFAULT_ROUNDS && more && got.read.some((p) => !tried.has(p)); round++) {
96
+ const wanted = got.read.filter((p) => !tried.has(p));
97
+ for (const p of wanted)
98
+ tried.add(p);
99
+ await more(wanted);
100
+ got = choose();
101
+ }
102
+ out.unread = got.read.filter((p) => !files.has(p));
103
+ return got;
104
+ };
105
+ if (format === "gitbook") {
106
+ const spaces = gitbookSpaces(all);
107
+ if (spaces.length > 1)
108
+ out.spaces = spaces;
109
+ }
110
+ else if (format === "vitepress") {
111
+ const { importChoices } = await import("@dogsbay/format-vitepress/migrate");
112
+ const got = await rounds(() => importChoices(files, all));
113
+ if (got.languages.length > 1)
114
+ out.languages = got.languages.map(({ id, label, isDefault, pages }) => ({ id, label, isDefault, pages }));
115
+ }
116
+ else if (format === "starlight") {
117
+ const { importChoices } = await import("@dogsbay/format-starlight/migrate");
118
+ const got = await rounds(() => importChoices(files, all));
119
+ if (got.languages.length > 1)
120
+ out.languages = got.languages.map(({ id, label, isDefault, pages }) => ({ id, label, isDefault, pages }));
121
+ else
122
+ out.products = got.products.map(({ id, label, pages }) => ({ id, label, pages }));
123
+ }
124
+ else {
125
+ const { importChoices } = await import("@dogsbay/format-docusaurus/migrate");
126
+ out.docs = importChoices(files, all).products.map((p) => ({
127
+ name: p.name,
128
+ instance: p.instance,
129
+ label: p.label,
130
+ versions: p.versions.map((v) => ({ id: v.id, name: v.name, ...(v.label ? { label: v.label } : {}), isDefault: v.isDefault, ...(v.prerelease ? { prerelease: true } : {}) })),
131
+ }));
132
+ }
133
+ return out;
134
+ }
135
+ /**
136
+ * The files a host gives up front, read as text: pages, config, data and scripts the conversions
137
+ * read. Anything else (images, fonts, PDFs …) is listed only, and given when asked for.
138
+ */
139
+ export const TEXT_FILE = /\.(md|mdx|markdown|ya?ml|json|jsonc|json5|[cm]?[jt]sx?|vue|astro|toml|txt|csv|tsv|html?|css|svg)$/i;
140
+ /** Text a Dogsbay project holds (what the SSG and the playground read). */
141
+ export const PROJECT_TEXT_EXTENSIONS = [".md", ".yml", ".yaml", ".json", ".css", ".txt", ".csv", ".tsv"];
142
+ /** Images a Dogsbay project holds, as bytes. */
143
+ export const PROJECT_IMAGE_EXTENSIONS = [".png", ".jpg", ".jpeg", ".gif", ".webp", ".avif", ".svg", ".ico"];
144
+ const extOf = (path) => {
145
+ const name = path.slice(path.lastIndexOf("/") + 1).toLowerCase();
146
+ const dot = name.lastIndexOf(".");
147
+ return dot > 0 ? name.slice(dot) : "";
148
+ };
149
+ export const isProjectText = (path) => PROJECT_TEXT_EXTENSIONS.includes(extOf(path));
150
+ export const isProjectImage = (path) => PROJECT_IMAGE_EXTENSIONS.includes(extOf(path));
151
+ /**
152
+ * The files of an import's result a project keeps, as every host keeps them: text as text, images
153
+ * as bytes; anything else (a PDF, a font) is left out. The playground's store holds only these, so
154
+ * a host that wrote more would give a different project.
155
+ */
156
+ export function projectFiles(files) {
157
+ const out = new Map();
158
+ for (const [path, value] of files) {
159
+ if (isProjectText(path))
160
+ out.set(path, typeof value === "string" ? value : new TextDecoder().decode(value));
161
+ else if (isProjectImage(path))
162
+ out.set(path, typeof value === "string" ? new TextEncoder().encode(value) : value);
163
+ }
164
+ return new Map([...out].sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0)));
165
+ }
166
+ /**
167
+ * A site's build output and caches: never given up front (a host lists them; a conversion that
168
+ * asks for one still gets it). Generated bundles there can be hundreds of MB.
169
+ */
170
+ export const BUILD_OUTPUT = /(^|\/)(\.vitepress\/(cache|dist)|\.docusaurus|\.astro|\.cache|build|dist|\.next|\.nuxt|\.output)\//;
171
+ /** The TypeScript examples converted in one import, at most (characters): Cloudflare's whole site has about 2 MB of them. */
172
+ export const EXAMPLES_BYTES = 8 * 1024 * 1024;
173
+ /**
174
+ * Starlight's `typescriptToJs`, each example converted once whatever the round (an import runs the
175
+ * conversion again each round), and at most `maxChars` of examples in all: past it an example keeps
176
+ * its TypeScript only (the conversion takes a failed one so, and says so). A repo's examples are its
177
+ * own text: a hostile one must not keep the host formatting for minutes.
178
+ */
179
+ export function cappedTypescriptToJs(convert, maxChars = EXAMPLES_BYTES) {
180
+ const converted = new Map();
181
+ let chars = 0;
182
+ return (ts) => {
183
+ let done = converted.get(ts);
184
+ if (!done) {
185
+ chars += ts.length;
186
+ done = chars > maxChars ? Promise.reject(new Error(`over the ${maxChars / 1024 / 1024} MB of TypeScript examples converted in one import`)) : convert(ts);
187
+ converted.set(ts, done);
188
+ }
189
+ return done;
190
+ };
191
+ }
192
+ /** A Starlight site's Astro config, at the site's folder. */
193
+ const ASTRO_CONFIG = /^astro\.config\.(js|ts|mjs|mts)$/;
194
+ /**
195
+ * The Starlight adapter whose site this is (paths from the site's folder, `read` its text): Cloudflare's,
196
+ * when an `astro.config.*` names developers.cloudflare.com as its `site`. As the playground offers it.
197
+ */
198
+ export function starlightAdapterFor(paths, read) {
199
+ for (const p of paths) {
200
+ if (ASTRO_CONFIG.test(p) && /\bsite:\s*["'`]https?:\/\/developers\.cloudflare\.com\/?["'`]/.test(read(p) ?? ""))
201
+ return "cloudflare";
202
+ }
203
+ return undefined;
204
+ }
package/package.json CHANGED
@@ -1,6 +1,42 @@
1
1
  {
2
2
  "name": "@dogsbay/import",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.2.0-beta.163",
4
+ "description": "A site made with another tool (GitBook, VitePress, Docusaurus, Starlight) as a Dogsbay project: the one import the playground and `dogsbay import` run, over a map of files, asking the host for what it needs in rounds. No Node.",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js"
12
+ }
13
+ },
14
+ "files": [
15
+ "dist",
16
+ "README.md"
17
+ ],
18
+ "dependencies": {
19
+ "@dogsbay/detect": "0.2.0-beta.163",
20
+ "@dogsbay/format-gitbook": "0.2.0-beta.163",
21
+ "@dogsbay/format-starlight": "0.2.0-beta.163",
22
+ "@dogsbay/format-docusaurus": "0.2.0-beta.163",
23
+ "@dogsbay/format-vitepress": "0.2.0-beta.163",
24
+ "@dogsbay/types": "0.2.0-beta.163"
25
+ },
26
+ "devDependencies": {
27
+ "@types/node": "^22.20.1",
28
+ "typescript": "^5.9.3",
29
+ "vitest": "^4.1.10"
30
+ },
31
+ "license": "MIT",
32
+ "repository": {
33
+ "type": "git",
34
+ "url": "https://github.com/dogsbay/dogsbay.git",
35
+ "directory": "packages/import"
36
+ },
37
+ "scripts": {
38
+ "build": "tsc",
39
+ "test": "vitest run",
40
+ "test:watch": "vitest"
41
+ }
6
42
  }