rlz-web-slides 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/README.md +157 -0
- package/lib/components/DrawingLayer.d.ts +2 -0
- package/lib/components/Panel.d.ts +12 -0
- package/lib/components/Slide.d.ts +9 -0
- package/lib/components/SlideNumber.d.ts +6 -0
- package/lib/index.js +8088 -0
- package/lib/player/Presentation.d.ts +10 -0
- package/lib/player/SlideContext.d.ts +11 -0
- package/lib/public-api.d.ts +7 -0
- package/lib/slides/DecoratedSlide.d.ts +12 -0
- package/lib/theme.d.ts +98 -0
- package/package.json +80 -0
- package/scripts/build-slides.d.mts +17 -0
- package/scripts/build-slides.mjs +165 -0
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type HTMLAttributes, type ReactNode } from 'react';
|
|
2
|
+
export interface PresentationProps extends Omit<HTMLAttributes<HTMLDivElement>, 'children' | 'onChange'> {
|
|
3
|
+
slides: readonly ReactNode[];
|
|
4
|
+
mode?: 'embedded' | 'fullscreen';
|
|
5
|
+
initialSlide?: number;
|
|
6
|
+
onSlideChange?: (index: number) => void;
|
|
7
|
+
controls?: boolean;
|
|
8
|
+
}
|
|
9
|
+
/** A scoped player. Keyboard navigation belongs to the focused player only. */
|
|
10
|
+
export declare function Presentation({ slides, mode, initialSlide, onSlideChange, controls, ...props }: PresentationProps): import("@emotion/react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type ReactNode } from 'react';
|
|
2
|
+
export declare function SlideCountProvider({ slideCount, children }: {
|
|
3
|
+
slideCount: number;
|
|
4
|
+
children: ReactNode;
|
|
5
|
+
}): import("@emotion/react/jsx-runtime").JSX.Element;
|
|
6
|
+
export declare function CurrentSlideProvider({ slideNumber, children }: {
|
|
7
|
+
slideNumber: number;
|
|
8
|
+
children: ReactNode;
|
|
9
|
+
}): import("@emotion/react/jsx-runtime").JSX.Element;
|
|
10
|
+
export declare function useSlideCount(): number;
|
|
11
|
+
export declare function useCurrentSlideNumber(): number;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { Slide, type SlideProps } from './components/Slide';
|
|
2
|
+
export { Panel, type PanelProps } from './components/Panel';
|
|
3
|
+
export { SlideNumber, type SlideNumberProps } from './components/SlideNumber';
|
|
4
|
+
export { DecoratedSlide, type DecoratedSlideProps } from './slides/DecoratedSlide';
|
|
5
|
+
export { Presentation, type PresentationProps } from './player/Presentation';
|
|
6
|
+
export { SlidesThemeProvider, resolveSlidesTheme, useSlidesTheme, type AuroraBackgroundOptions, type BackgroundColor, type BackgroundVariants, type GridBackgroundOptions, type MeshBackgroundOptions, type SpotlightBackgroundOptions, type SlidesTheme, type SlidesThemeInput, type TextureBackgroundOptions } from './theme';
|
|
7
|
+
export { css } from '@emotion/react';
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import { type SlideProps } from '../components/Slide';
|
|
3
|
+
import type { Interpolation, Theme } from '@emotion/react';
|
|
4
|
+
export interface DecoratedSlideProps extends SlideProps {
|
|
5
|
+
headerLeft?: ReactNode;
|
|
6
|
+
headerRight?: ReactNode;
|
|
7
|
+
footerLeft?: ReactNode;
|
|
8
|
+
footerRight?: ReactNode;
|
|
9
|
+
css?: Interpolation<Theme>;
|
|
10
|
+
}
|
|
11
|
+
/** Optional content regions shared by the themed slide templates. */
|
|
12
|
+
export declare function DecoratedSlide({ css, headerLeft, headerRight, footerLeft, footerRight, children, drawing, ...props }: DecoratedSlideProps): import("@emotion/react/jsx-runtime").JSX.Element;
|
package/lib/theme.d.ts
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import type { CSSObject } from '@emotion/react';
|
|
2
|
+
import { type ReactNode } from 'react';
|
|
3
|
+
export type ColorName = 'accent-1' | 'accent-2' | 'accent-3' | 'light' | 'dark' | 'neutral';
|
|
4
|
+
export type BackgroundColor = ColorName | (string & {});
|
|
5
|
+
export type Background = (color?: BackgroundColor) => CSSObject;
|
|
6
|
+
export interface TextureBackgroundOptions {
|
|
7
|
+
base?: BackgroundColor;
|
|
8
|
+
dot?: BackgroundColor;
|
|
9
|
+
size?: number;
|
|
10
|
+
}
|
|
11
|
+
export interface AuroraBackgroundOptions {
|
|
12
|
+
base?: BackgroundColor;
|
|
13
|
+
lowerLeft?: BackgroundColor;
|
|
14
|
+
upperRight?: BackgroundColor;
|
|
15
|
+
center?: BackgroundColor;
|
|
16
|
+
}
|
|
17
|
+
export interface MeshBackgroundOptions {
|
|
18
|
+
base?: BackgroundColor;
|
|
19
|
+
colors?: [BackgroundColor, BackgroundColor, BackgroundColor];
|
|
20
|
+
}
|
|
21
|
+
export interface GridBackgroundOptions {
|
|
22
|
+
base?: BackgroundColor;
|
|
23
|
+
line?: BackgroundColor;
|
|
24
|
+
size?: number;
|
|
25
|
+
}
|
|
26
|
+
export interface SpotlightBackgroundOptions {
|
|
27
|
+
base?: BackgroundColor;
|
|
28
|
+
light?: BackgroundColor;
|
|
29
|
+
position?: string;
|
|
30
|
+
}
|
|
31
|
+
export interface BackgroundVariants {
|
|
32
|
+
solid: Background;
|
|
33
|
+
gradient: Background;
|
|
34
|
+
texture: (options?: TextureBackgroundOptions) => CSSObject;
|
|
35
|
+
aurora: (options?: AuroraBackgroundOptions) => CSSObject;
|
|
36
|
+
mesh: (options?: MeshBackgroundOptions) => CSSObject;
|
|
37
|
+
grid: (options?: GridBackgroundOptions) => CSSObject;
|
|
38
|
+
spotlight: (options?: SpotlightBackgroundOptions) => CSSObject;
|
|
39
|
+
}
|
|
40
|
+
export type Backgrounds = BackgroundVariants;
|
|
41
|
+
export interface ThemeColors {
|
|
42
|
+
'accent-1': string;
|
|
43
|
+
'accent-2': string;
|
|
44
|
+
'accent-3': string;
|
|
45
|
+
light: string;
|
|
46
|
+
textLight: string;
|
|
47
|
+
dark: string;
|
|
48
|
+
textDark: string;
|
|
49
|
+
neutral: string;
|
|
50
|
+
muted: string;
|
|
51
|
+
}
|
|
52
|
+
export type ShadowLevel = 'low' | 'medium' | 'high';
|
|
53
|
+
export interface SpacingScale {
|
|
54
|
+
tight: number;
|
|
55
|
+
compact: number;
|
|
56
|
+
half: number;
|
|
57
|
+
comfortable: number;
|
|
58
|
+
base: number;
|
|
59
|
+
double: number;
|
|
60
|
+
}
|
|
61
|
+
export interface RadiusScale {
|
|
62
|
+
quarter: number;
|
|
63
|
+
half: number;
|
|
64
|
+
base: number;
|
|
65
|
+
double: number;
|
|
66
|
+
}
|
|
67
|
+
export interface SlidesTheme {
|
|
68
|
+
width: number;
|
|
69
|
+
height: number;
|
|
70
|
+
fontFamily: string;
|
|
71
|
+
fontSize: number;
|
|
72
|
+
spacing: number;
|
|
73
|
+
radius: number;
|
|
74
|
+
spacings: SpacingScale;
|
|
75
|
+
radii: RadiusScale;
|
|
76
|
+
colors: ThemeColors;
|
|
77
|
+
backgrounds: Backgrounds;
|
|
78
|
+
shadows: Record<ShadowLevel, CSSObject>;
|
|
79
|
+
typography: CSSObject;
|
|
80
|
+
motion: {
|
|
81
|
+
duration: number;
|
|
82
|
+
easing: string;
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
export type SlidesThemeInput = Partial<Omit<SlidesTheme, 'colors' | 'motion' | 'backgrounds' | 'shadows' | 'spacings' | 'radii'>> & {
|
|
86
|
+
backgrounds?: Partial<Backgrounds>;
|
|
87
|
+
shadows?: Partial<SlidesTheme['shadows']>;
|
|
88
|
+
colors?: Partial<SlidesTheme['colors']>;
|
|
89
|
+
motion?: Partial<SlidesTheme['motion']>;
|
|
90
|
+
};
|
|
91
|
+
/** Resolve every field, deriving background factories from the final palette. */
|
|
92
|
+
export declare function resolveSlidesTheme(input?: SlidesThemeInput): SlidesTheme;
|
|
93
|
+
/** Nested providers inherit raw overrides so derived colors remain reactive. */
|
|
94
|
+
export declare function SlidesThemeProvider({ theme, children }: {
|
|
95
|
+
theme?: SlidesThemeInput;
|
|
96
|
+
children: ReactNode;
|
|
97
|
+
}): import("@emotion/react/jsx-runtime").JSX.Element;
|
|
98
|
+
export declare function useSlidesTheme(): SlidesTheme;
|
package/package.json
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "rlz-web-slides",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"repository": {
|
|
5
|
+
"type": "git",
|
|
6
|
+
"url": "git+https://github.com/rlz/web-slides.git"
|
|
7
|
+
},
|
|
8
|
+
"homepage": "https://rlz.github.io/web-slides/",
|
|
9
|
+
"license": "MIT",
|
|
10
|
+
"keywords": [
|
|
11
|
+
"react",
|
|
12
|
+
"slides",
|
|
13
|
+
"presentation",
|
|
14
|
+
"emotion"
|
|
15
|
+
],
|
|
16
|
+
"engines": {
|
|
17
|
+
"node": ">=22.12.0"
|
|
18
|
+
},
|
|
19
|
+
"type": "module",
|
|
20
|
+
"scripts": {
|
|
21
|
+
"dev": "vite",
|
|
22
|
+
"build": "tsc -b && vite build",
|
|
23
|
+
"build:pages": "tsc -b && vite build --mode pages",
|
|
24
|
+
"export:html": "node scripts/export-demo.mjs",
|
|
25
|
+
"lint": "oxlint && prettier --check .",
|
|
26
|
+
"format": "prettier --write .",
|
|
27
|
+
"preview": "vite preview",
|
|
28
|
+
"start": "vite",
|
|
29
|
+
"demo": "vite --mode demo",
|
|
30
|
+
"build:lib": "vite build --config vite.lib.config.ts && tsc -p tsconfig.lib.json",
|
|
31
|
+
"test": "npm run build:lib && node --test tests/*.test.mjs",
|
|
32
|
+
"prepack": "npm run build:lib"
|
|
33
|
+
},
|
|
34
|
+
"dependencies": {
|
|
35
|
+
"@fortawesome/fontawesome-svg-core": "^7.3.1",
|
|
36
|
+
"@fortawesome/free-solid-svg-icons": "^7.3.1",
|
|
37
|
+
"@fortawesome/react-fontawesome": "^3.5.0",
|
|
38
|
+
"@vitejs/plugin-react": "^6.0.4",
|
|
39
|
+
"colord": "^2.10.0",
|
|
40
|
+
"culori": "^4.0.2",
|
|
41
|
+
"vite": "^8.2.0"
|
|
42
|
+
},
|
|
43
|
+
"devDependencies": {
|
|
44
|
+
"@emotion/react": "^11.14.0",
|
|
45
|
+
"@types/culori": "^4.0.1",
|
|
46
|
+
"@types/node": "^26.3.0",
|
|
47
|
+
"@types/react": "^19.2.17",
|
|
48
|
+
"@types/react-dom": "^19.2.3",
|
|
49
|
+
"oxlint": "^1.75.0",
|
|
50
|
+
"prettier": "^3.9.6",
|
|
51
|
+
"react": "^19.2.8",
|
|
52
|
+
"react-dom": "^19.2.8",
|
|
53
|
+
"typescript": "^7.0.2"
|
|
54
|
+
},
|
|
55
|
+
"description": "Composable React slides, themes, an embedded player and standalone HTML export",
|
|
56
|
+
"main": "./lib/index.js",
|
|
57
|
+
"types": "./lib/index.d.ts",
|
|
58
|
+
"exports": {
|
|
59
|
+
".": {
|
|
60
|
+
"types": "./lib/index.d.ts",
|
|
61
|
+
"import": "./lib/index.js"
|
|
62
|
+
},
|
|
63
|
+
"./build": {
|
|
64
|
+
"types": "./scripts/build-slides.d.mts",
|
|
65
|
+
"import": "./scripts/build-slides.mjs"
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
"files": [
|
|
69
|
+
"lib",
|
|
70
|
+
"scripts/build-slides.mjs",
|
|
71
|
+
"scripts/build-slides.d.mts",
|
|
72
|
+
"README.md",
|
|
73
|
+
"LICENSE"
|
|
74
|
+
],
|
|
75
|
+
"peerDependencies": {
|
|
76
|
+
"@emotion/react": "^11.14.0",
|
|
77
|
+
"react": "^19.2.8",
|
|
78
|
+
"react-dom": "^19.2.8"
|
|
79
|
+
}
|
|
80
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { SlidesThemeInput } from '../lib/theme'
|
|
2
|
+
|
|
3
|
+
export interface SlideModule {
|
|
4
|
+
file: string
|
|
5
|
+
exportName?: string
|
|
6
|
+
props?: Record<string, unknown>
|
|
7
|
+
}
|
|
8
|
+
export interface BuildSlidesOptions {
|
|
9
|
+
title?: string
|
|
10
|
+
theme?: SlidesThemeInput
|
|
11
|
+
publicDir?: string
|
|
12
|
+
}
|
|
13
|
+
export function buildSlides(
|
|
14
|
+
slides: (string | SlideModule)[],
|
|
15
|
+
filename: string,
|
|
16
|
+
options?: BuildSlidesOptions
|
|
17
|
+
): Promise<string>
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { build } from 'vite'
|
|
2
|
+
import react from '@vitejs/plugin-react'
|
|
3
|
+
import { mkdir, readdir, readFile, writeFile } from 'node:fs/promises'
|
|
4
|
+
import { dirname, extname, resolve } from 'node:path'
|
|
5
|
+
import { fileURLToPath } from 'node:url'
|
|
6
|
+
|
|
7
|
+
const libraryEntry = fileURLToPath(new URL('../lib/index.js', import.meta.url))
|
|
8
|
+
const mimeTypes = {
|
|
9
|
+
'.png': 'image/png',
|
|
10
|
+
'.jpg': 'image/jpeg',
|
|
11
|
+
'.jpeg': 'image/jpeg',
|
|
12
|
+
'.svg': 'image/svg+xml',
|
|
13
|
+
'.gif': 'image/gif',
|
|
14
|
+
'.webp': 'image/webp',
|
|
15
|
+
'.avif': 'image/avif',
|
|
16
|
+
'.ico': 'image/x-icon',
|
|
17
|
+
'.otf': 'font/otf',
|
|
18
|
+
'.ttf': 'font/ttf',
|
|
19
|
+
'.woff': 'font/woff',
|
|
20
|
+
'.woff2': 'font/woff2',
|
|
21
|
+
'.mp4': 'video/mp4',
|
|
22
|
+
'.webm': 'video/webm',
|
|
23
|
+
'.mp3': 'audio/mpeg',
|
|
24
|
+
'.wav': 'audio/wav',
|
|
25
|
+
'.pdf': 'application/pdf'
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Bundle React slide modules into a standalone, interactive HTML file.
|
|
30
|
+
* @param {(string | {file: string, exportName?: string, props?: Record<string, unknown>})[]} slides
|
|
31
|
+
* @param {string} filename Output path, with or without .html.
|
|
32
|
+
* @param {{title?: string, theme?: import('../lib/theme').SlidesThemeInput, publicDir?: string}} options
|
|
33
|
+
* @returns {Promise<string>} Absolute output path.
|
|
34
|
+
*/
|
|
35
|
+
export async function buildSlides(slides, filename, options = {}) {
|
|
36
|
+
if (!Array.isArray(slides) || slides.length === 0)
|
|
37
|
+
throw new TypeError('Pass at least one slide module')
|
|
38
|
+
if (typeof filename !== 'string' || !filename.trim())
|
|
39
|
+
throw new TypeError('Pass a non-empty filename')
|
|
40
|
+
const root = process.cwd()
|
|
41
|
+
const descriptors = slides.map((slide) =>
|
|
42
|
+
typeof slide === 'string' ? { file: slide } : slide
|
|
43
|
+
)
|
|
44
|
+
for (const slide of descriptors) {
|
|
45
|
+
if (!slide || typeof slide.file !== 'string' || !slide.file.trim())
|
|
46
|
+
throw new TypeError('Every slide needs a module file path')
|
|
47
|
+
}
|
|
48
|
+
const publicDir = resolve(root, options.publicDir ?? 'public')
|
|
49
|
+
const entryId = resolve(root, '.web-slides-entry.tsx')
|
|
50
|
+
const source = [
|
|
51
|
+
'import { createElement } from "react"',
|
|
52
|
+
'import { createRoot } from "react-dom/client"',
|
|
53
|
+
`import { Presentation, SlidesThemeProvider } from ${JSON.stringify(libraryEntry)}`,
|
|
54
|
+
...descriptors.map(
|
|
55
|
+
(slide, index) =>
|
|
56
|
+
`import { ${JSON.stringify(slide.exportName ?? 'default')} as Slide${index} } from ${JSON.stringify(resolve(root, slide.file))}`
|
|
57
|
+
),
|
|
58
|
+
`const slides = [${descriptors.map((slide, index) => `createElement(Slide${index}, ${JSON.stringify(slide.props ?? {})})`).join(',')}]`,
|
|
59
|
+
`createRoot(document.getElementById('root')).render(createElement(SlidesThemeProvider, {theme: ${JSON.stringify(options.theme ?? {})}}, createElement(Presentation, {slides, mode: 'fullscreen'})))`
|
|
60
|
+
].join('\n')
|
|
61
|
+
const result = await build({
|
|
62
|
+
configFile: false,
|
|
63
|
+
root,
|
|
64
|
+
publicDir,
|
|
65
|
+
plugins: [
|
|
66
|
+
{
|
|
67
|
+
name: 'web-slides-entry',
|
|
68
|
+
resolveId(id) {
|
|
69
|
+
if (id === entryId) return entryId
|
|
70
|
+
},
|
|
71
|
+
load(id) {
|
|
72
|
+
if (id === entryId) return source
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
react({ jsxImportSource: '@emotion/react' })
|
|
76
|
+
],
|
|
77
|
+
define: { 'process.env.NODE_ENV': JSON.stringify('production') },
|
|
78
|
+
resolve: { dedupe: ['react', 'react-dom', '@emotion/react'] },
|
|
79
|
+
build: {
|
|
80
|
+
write: false,
|
|
81
|
+
copyPublicDir: false,
|
|
82
|
+
minify: false,
|
|
83
|
+
lib: {
|
|
84
|
+
entry: entryId,
|
|
85
|
+
name: 'WebSlidesPresentation',
|
|
86
|
+
formats: ['iife']
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
})
|
|
90
|
+
const outputs = (Array.isArray(result) ? result : [result]).flatMap(
|
|
91
|
+
(bundle) => bundle.output
|
|
92
|
+
)
|
|
93
|
+
let javascript = outputs
|
|
94
|
+
.filter((output) => output.type === 'chunk')
|
|
95
|
+
.map((output) => output.code)
|
|
96
|
+
.join('\n')
|
|
97
|
+
let css = outputs
|
|
98
|
+
.filter(
|
|
99
|
+
(output) =>
|
|
100
|
+
output.type === 'asset' && output.fileName.endsWith('.css')
|
|
101
|
+
)
|
|
102
|
+
.map((output) => String(output.source))
|
|
103
|
+
.join('\n')
|
|
104
|
+
for (const output of outputs) {
|
|
105
|
+
if (output.type === 'asset' && !output.fileName.endsWith('.css'))
|
|
106
|
+
throw new Error(`Asset could not be inlined: ${output.fileName}`)
|
|
107
|
+
}
|
|
108
|
+
// Vite library mode inlines imported assets. Public assets need explicit embedding.
|
|
109
|
+
for (const file of await publicFiles(publicDir)) {
|
|
110
|
+
const url = `/${file}`
|
|
111
|
+
const escaped = url.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
112
|
+
const pattern = new RegExp(
|
|
113
|
+
/(?<=["'`(])/.source + escaped + /(?=["'`)])/.source,
|
|
114
|
+
'g'
|
|
115
|
+
)
|
|
116
|
+
if (!javascript.match(pattern) && !css.match(pattern)) continue
|
|
117
|
+
const mime = mimeTypes[extname(file).toLowerCase()]
|
|
118
|
+
if (!mime)
|
|
119
|
+
throw new Error(
|
|
120
|
+
`Import this resource from source instead of public: ${file}`
|
|
121
|
+
)
|
|
122
|
+
const data = `data:${mime};base64,${(await readFile(resolve(publicDir, file))).toString('base64')}`
|
|
123
|
+
javascript = javascript.replace(pattern, () => data)
|
|
124
|
+
css = css.replace(pattern, () => data)
|
|
125
|
+
}
|
|
126
|
+
const title = (options.title ?? 'Presentation').replace(
|
|
127
|
+
/[&<>"']/g,
|
|
128
|
+
(character) =>
|
|
129
|
+
({
|
|
130
|
+
'&': '&',
|
|
131
|
+
'<': '<',
|
|
132
|
+
'>': '>',
|
|
133
|
+
'"': '"',
|
|
134
|
+
"'": '''
|
|
135
|
+
})[character]
|
|
136
|
+
)
|
|
137
|
+
const html = `<!doctype html>\n<html lang="ru">\n<head>\n<meta charset="UTF-8">\n<meta name="viewport" content="width=device-width, initial-scale=1">\n<title>${title}</title>\n<style>body { margin: 0; background: #171c17; }\n${css.replace(/<\/style/gi, '<\\/style')}</style>\n</head>\n<body>\n<div id="root"></div>\n<script>${javascript.replace(/<\/script/gi, '<\\/script')}</script>\n</body>\n</html>\n`
|
|
138
|
+
const outputPath = resolve(
|
|
139
|
+
root,
|
|
140
|
+
filename.endsWith('.html') ? filename : `${filename}.html`
|
|
141
|
+
)
|
|
142
|
+
await mkdir(dirname(outputPath), { recursive: true })
|
|
143
|
+
await writeFile(outputPath, html)
|
|
144
|
+
return outputPath
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async function publicFiles(directory, prefix = '') {
|
|
148
|
+
let entries
|
|
149
|
+
try {
|
|
150
|
+
entries = await readdir(resolve(directory, prefix), {
|
|
151
|
+
withFileTypes: true
|
|
152
|
+
})
|
|
153
|
+
} catch (error) {
|
|
154
|
+
if (error.code === 'ENOENT') return []
|
|
155
|
+
throw error
|
|
156
|
+
}
|
|
157
|
+
const result = []
|
|
158
|
+
for (const entry of entries) {
|
|
159
|
+
const file = prefix ? `${prefix}/${entry.name}` : entry.name
|
|
160
|
+
if (entry.isDirectory())
|
|
161
|
+
result.push(...(await publicFiles(directory, file)))
|
|
162
|
+
else if (entry.isFile()) result.push(file)
|
|
163
|
+
}
|
|
164
|
+
return result.sort((a, b) => b.length - a.length)
|
|
165
|
+
}
|