fragment-tools 0.2.11 → 0.2.13

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.
Files changed (67) hide show
  1. package/package.json +12 -11
  2. package/src/cli/build.js +1 -0
  3. package/src/cli/create.js +22 -4
  4. package/src/cli/createConfig.js +2 -2
  5. package/src/cli/getEntries.js +10 -1
  6. package/src/cli/plugins/hot-shader-replacement.js +54 -16
  7. package/src/cli/plugins/save.js +97 -38
  8. package/src/cli/prompts.js +89 -36
  9. package/src/cli/run.js +1 -1
  10. package/src/cli/templates/blank/index.ts +1 -1
  11. package/src/cli/templates/default/index.js +10 -2
  12. package/src/cli/templates/default/index.ts +5 -2
  13. package/src/cli/templates/fragment-gl/index.ts +1 -1
  14. package/src/cli/templates/p5/index.ts +1 -1
  15. package/src/cli/templates/p5-webgl/index.ts +1 -1
  16. package/src/cli/templates/three-fragment/index.js +5 -3
  17. package/src/cli/templates/three-fragment/index.ts +5 -4
  18. package/src/cli/templates/three-orthographic/index.js +6 -1
  19. package/src/cli/templates/three-orthographic/index.ts +6 -2
  20. package/src/cli/templates/three-perspective/index.js +6 -1
  21. package/src/cli/templates/three-perspective/index.ts +6 -2
  22. package/src/client/app/actions/resize.js +8 -1
  23. package/src/client/app/attachments/draggable.js +93 -0
  24. package/src/client/app/client.js +90 -18
  25. package/src/client/app/components/IconFlip.svelte +46 -0
  26. package/src/client/app/hooks.js +25 -1
  27. package/src/client/app/lib/canvas-recorder/CanvasRecorder.js +95 -3
  28. package/src/client/app/lib/canvas-recorder/FrameRecorder.js +45 -3
  29. package/src/client/app/lib/canvas-recorder/GIFRecorder.js +72 -13
  30. package/src/client/app/lib/canvas-recorder/MediaBunnyRecorder.js +43 -9
  31. package/src/client/app/lib/canvas-recorder/utils.js +18 -9
  32. package/src/client/app/modules/Params.svelte +1 -0
  33. package/src/client/app/renderers/2DRenderer.js +20 -16
  34. package/src/client/app/renderers/P5GLRenderer.js +13 -5
  35. package/src/client/app/renderers/P5Renderer.js +9 -1
  36. package/src/client/app/renderers/THREERenderer.js +63 -48
  37. package/src/client/app/state/Sketch.svelte.js +150 -10
  38. package/src/client/app/state/errors.svelte.js +19 -0
  39. package/src/client/app/state/exports.svelte.js +14 -1
  40. package/src/client/app/state/rendering.svelte.js +90 -13
  41. package/src/client/app/state/sketches.svelte.js +43 -7
  42. package/src/client/app/state/utils.svelte.js +49 -0
  43. package/src/client/app/ui/Field.svelte +63 -16
  44. package/src/client/app/ui/FieldSection.svelte +4 -4
  45. package/src/client/app/ui/ParamsOutput.svelte +7 -5
  46. package/src/client/app/ui/SketchRenderer.svelte +21 -0
  47. package/src/client/app/ui/fields/ButtonInput.svelte +2 -0
  48. package/src/client/app/ui/fields/CheckboxInput.svelte +13 -11
  49. package/src/client/app/ui/fields/ColorInput.svelte +16 -11
  50. package/src/client/app/ui/fields/GradientInput.svelte +607 -0
  51. package/src/client/app/ui/fields/Input.svelte +10 -6
  52. package/src/client/app/ui/fields/IntervalInput.svelte +27 -35
  53. package/src/client/app/ui/fields/NumberInput.svelte +51 -13
  54. package/src/client/app/ui/fields/PaletteInput.svelte +181 -0
  55. package/src/client/app/ui/fields/ProgressInput.svelte +44 -16
  56. package/src/client/app/ui/fields/TextareaInput.svelte +93 -0
  57. package/src/client/app/utils/canvas.utils.js +105 -28
  58. package/src/client/app/utils/color.utils.js +74 -17
  59. package/src/client/app/utils/fields.utils.js +70 -17
  60. package/src/client/app/utils/file.utils.js +86 -31
  61. package/src/client/app/utils/glsl.utils.js +11 -2
  62. package/src/client/app/utils/glslErrors.js +31 -21
  63. package/src/client/app/utils/index.js +28 -12
  64. package/src/client/main.js +7 -1
  65. package/src/types/global.d.ts +143 -0
  66. package/src/types/props.d.ts +41 -15
  67. package/tsconfig.json +1 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fragment-tools",
3
- "version": "0.2.11",
3
+ "version": "0.2.13",
4
4
  "description": "A web development environment for creative coding",
5
5
  "main": "./src/index.js",
6
6
  "bin": {
@@ -24,24 +24,25 @@
24
24
  },
25
25
  "homepage": "https://github.com/raphaelameaume/fragment#readme",
26
26
  "dependencies": {
27
- "@clack/core": "^0.4.2",
28
- "@sveltejs/vite-plugin-svelte": "^5.0.3",
29
- "changedpi": "^1.0.4",
30
- "convert-length": "^1.0.1",
27
+ "@clack/core": "^1.3.1",
28
+ "@sveltejs/vite-plugin-svelte": "^7.1.2",
29
+ "changedpi": "1.0.4",
30
+ "convert-length": "1.0.1",
31
31
  "get-port": "^7.1.0",
32
- "gifenc": "^1.0.3",
32
+ "gifenc": "1.0.3",
33
33
  "is-unicode-supported": "^2.0.0",
34
- "kleur": "^4.1.4",
34
+ "kleur": "^4.1.5",
35
35
  "mediabunny": "^1.13.3",
36
36
  "milliparsec": "^5.1.0",
37
- "sade": "^1.7.4",
38
- "svelte": "^5.19.0",
39
- "vite": "^6.3.5",
40
- "ws": "^8.2.3"
37
+ "sade": "^1.8.1",
38
+ "svelte": "^5.55.9",
39
+ "vite": "^8.0.14",
40
+ "ws": "^8.20.1"
41
41
  },
42
42
  "devDependencies": {
43
43
  "@changesets/cli": "^2.29.7",
44
44
  "@svitejs/changesets-changelog-github-compact": "^1.2.0",
45
+ "@types/node": "^25.9.1",
45
46
  "@types/p5": "^1.7.6",
46
47
  "@types/three": "^0.174.0",
47
48
  "prettier": "^3.2.5",
package/src/cli/build.js CHANGED
@@ -83,6 +83,7 @@ export async function build(entry, options) {
83
83
  placeholder: `/`,
84
84
  hint: '(Hit Enter to validate)',
85
85
  initialValue: base,
86
+ defaultValue: '/',
86
87
  });
87
88
 
88
89
  handleCancelledPrompt(base, prefix);
package/src/cli/create.js CHANGED
@@ -18,8 +18,8 @@ import {
18
18
  * Create a new sketch
19
19
  * @param {string} entry
20
20
  * @param {object} options
21
- * @param {string} options.templateName
22
- * @param {boolean} options.typescript
21
+ * @param {string} [options.templateName]
22
+ * @param {boolean} [options.typescript]
23
23
  */
24
24
  export async function create(entry, { templateName, typescript } = {}) {
25
25
  const cwd = process.cwd();
@@ -28,7 +28,10 @@ export async function create(entry, { templateName, typescript } = {}) {
28
28
  try {
29
29
  log.message(`${magenta(entry)}\n`, prefix);
30
30
 
31
- let dir, name;
31
+ /** @type {string |undefined} */
32
+ let dir;
33
+ /** @type {string |undefined} */
34
+ let name;
32
35
 
33
36
  if (entry) {
34
37
  const { dir: entryDir, base: entryBase } = path.parse(entry);
@@ -42,6 +45,7 @@ export async function create(entry, { templateName, typescript } = {}) {
42
45
  placeholder: '.',
43
46
  hint: '(hit Enter to use current directory)',
44
47
  initialValue: dir,
48
+ defaultValue: '.',
45
49
  });
46
50
 
47
51
  handleCancelledPrompt(dir, prefix);
@@ -56,7 +60,9 @@ export async function create(entry, { templateName, typescript } = {}) {
56
60
  hint: '(hit Enter to validate)',
57
61
  initialValue: name,
58
62
  validate: (value) => {
59
- if (value.length === 0) return `A name is required.`;
63
+ if (!value || value.length === 0) return `A name is required.`;
64
+
65
+ return undefined;
60
66
  },
61
67
  });
62
68
 
@@ -64,6 +70,18 @@ export async function create(entry, { templateName, typescript } = {}) {
64
70
 
65
71
  name = name.replace(/\s/g, '-');
66
72
 
73
+ /**
74
+ * @typedef TemplateOption
75
+ * @property {string} name
76
+ * @property {string} description
77
+ * @property {string} path
78
+ * @property {string} label
79
+ * @property {string} hint
80
+ * @property {string} value
81
+ * @property {boolean} [isDefault=false]
82
+ */
83
+
84
+ /** @type {TemplateOption[]} */
67
85
  let templatesOptions = fs
68
86
  .readdirSync(file('./templates'))
69
87
  .map((dir) => {
@@ -1,7 +1,7 @@
1
1
  import path from 'node:path';
2
2
  import fs from 'node:fs';
3
3
  import url from 'node:url';
4
- import { defineConfig, loadConfigFromFile, mergeConfig } from 'vite';
4
+ import { defineConfig, mergeConfig } from 'vite';
5
5
  import { svelte } from '@sveltejs/vite-plugin-svelte';
6
6
 
7
7
  import checkDependencies from './plugins/check-dependencies.js';
@@ -66,7 +66,7 @@ export async function loadConfig({ cwd, filepath }) {
66
66
  * @param {boolean} [options.build=false]
67
67
  * @param {string} [configFilepath]
68
68
  * @param {string} [cwd=process.cwd()]
69
- * @returns {import('vite').UserConfig}
69
+ * @returns {Promise<import('vite').UserConfig>}
70
70
  */
71
71
  export async function createConfig(
72
72
  entries,
@@ -9,7 +9,8 @@ import { addExtension } from './utils.js';
9
9
  * Build entries from entry filepath or folder path
10
10
  * @param {string} entry
11
11
  * @param {string} cwd - Current working directory
12
- * @returns {string[]}
12
+ * @param {string} command - Current working directory
13
+ * @returns {Promise<string[]>}
13
14
  */
14
15
  export async function getEntries(
15
16
  entry,
@@ -17,10 +18,18 @@ export async function getEntries(
17
18
  command,
18
19
  prefix = log.prefix(command),
19
20
  ) {
21
+ /**
22
+ *
23
+ * @param {string} message
24
+ */
20
25
  const displayCommand = (message) => {
21
26
  p.note(bold(cyan(message)));
22
27
  };
23
28
 
29
+ /**
30
+ *
31
+ * @param {string} message
32
+ */
24
33
  const onError = (message) => {
25
34
  log.error(`Error\n`, prefix);
26
35
  log.warn(message);
@@ -4,11 +4,20 @@ import { readFile } from 'node:fs/promises';
4
4
  import { log, dim, green, yellow } from '../log.js';
5
5
 
6
6
  /**
7
- * @typedef {Object} ShaderUpdate
7
+ * @typedef Warning
8
+ * @property {string} type
9
+ * @property {string} importer
10
+ * @property {string} message
11
+ * @property {string} url
12
+ * @property {{ lineText: string }} location
13
+ */
14
+
15
+ /**
16
+ * @typedef ShaderUpdate
8
17
  * @property {string} filepath - The path of the shader on the filesystem
9
18
  * @property {string} source - The source code of the shader
10
19
  * @property {boolean} nohsr - Whether the shader can be injected on the fly or if the sketch needs to be fully reloaded
11
- * @property {string[]} warnings - Indicates whether the Wisdom component is present.
20
+ * @property {Warning[]} warnings - Indicates whether the Wisdom component is present.
12
21
  */
13
22
 
14
23
  /**
@@ -28,8 +37,11 @@ export default function hotShaderReplacement({ cwd = process.cwd(), wss }) {
28
37
  /(\/\*([^*]|[\r\n]|(\*+([^*\/]|[\r\n])))*\*+\/)|(\/\/.*)/gi;
29
38
  const base = process.cwd().split(path.sep).join(path.posix.sep);
30
39
 
40
+ /** @type Map<string, string[]> */
31
41
  let dependencies = new Map();
42
+ /** @type {string[]} */
32
43
  let shaders = [];
44
+ /** @type {import('vite').ModuleNode[]} */
33
45
  let modulesToReload = [];
34
46
 
35
47
  function reloadSketch() {
@@ -56,6 +68,12 @@ export default function hotShaderReplacement({ cwd = process.cwd(), wss }) {
56
68
  return clone;
57
69
  }
58
70
 
71
+ /**
72
+ *
73
+ * @param {string} shaderSource
74
+ * @param {string} shaderPath
75
+ * @returns
76
+ */
59
77
  function addShaderFilepath(shaderSource, shaderPath) {
60
78
  let keyword = `void main`;
61
79
  let shaderParts = shaderSource.split(keyword);
@@ -67,10 +85,21 @@ ${keyword}${shaderParts[1]}
67
85
  `;
68
86
  }
69
87
 
88
+ /**
89
+ *
90
+ * @param {string} shaderPath
91
+ * @returns {string}
92
+ */
70
93
  function getUnixPath(shaderPath) {
71
94
  return shaderPath.split(path.sep).join(path.posix.sep);
72
95
  }
73
96
 
97
+ /**
98
+ *
99
+ * @param {string} shaderSource
100
+ * @param {string} shaderPath
101
+ * @returns
102
+ */
74
103
  function compileGLSL(shaderSource, shaderPath) {
75
104
  // test if shader source contains hint to avoid shader injection
76
105
  const nohsr = ignoreRegex.test(shaderSource);
@@ -97,7 +126,8 @@ ${keyword}${shaderParts[1]}
97
126
  * @param {string} parentSource
98
127
  * @param {string} parentPath
99
128
  * @param {string[]} deps
100
- * @returns {}
129
+ * @param {Warning[]} warnings
130
+ * @returns {{ code: string, deps: string[], warnings: Warning[] }}
101
131
  */
102
132
  function resolveDependencies(
103
133
  parentSource,
@@ -163,7 +193,7 @@ ${keyword}${shaderParts[1]}
163
193
 
164
194
  const parents = dependencies.get(chunkUnixPath);
165
195
 
166
- if (!parents.includes(shaderPath)) {
196
+ if (parents && !parents.includes(shaderPath)) {
167
197
  parents.push(shaderPath);
168
198
  deps.push(chunkResolvedPath);
169
199
  } else {
@@ -201,7 +231,11 @@ ${keyword}${shaderParts[1]}
201
231
 
202
232
  return `${prefix}\n${chunkCode}`;
203
233
  } catch (error) {
204
- if (error.code === 'ENOENT') {
234
+ const err = /** @type {NodeJS.ErrnoException} */ (
235
+ error
236
+ );
237
+
238
+ if (err.code === 'ENOENT') {
205
239
  warnings.push({
206
240
  type: 'not found',
207
241
  message: `Cannot find ${chunkResolvedPath}`,
@@ -237,8 +271,7 @@ ${keyword}${shaderParts[1]}
237
271
 
238
272
  warnings.forEach((warning) => {
239
273
  const { location } = warning;
240
- const line = 1;
241
- const column = 4;
274
+
242
275
  log.message(`${yellow(warning.type)} ${warning.importer}`, prefix);
243
276
  console.log();
244
277
  console.log(` ${dim(location.lineText)}`);
@@ -291,8 +324,8 @@ ${keyword}${shaderParts[1]}
291
324
  name: 'fragment-plugin-hsr',
292
325
  config: () => ({
293
326
  optimizeDeps: {
294
- esbuildOptions: {
295
- loader: {
327
+ rolldownOptions: {
328
+ moduleTypes: {
296
329
  '.frag': 'text',
297
330
  '.vert': 'text',
298
331
  '.glsl': 'text',
@@ -305,7 +338,7 @@ ${keyword}${shaderParts[1]}
305
338
  configureServer(_server) {
306
339
  server = _server;
307
340
  },
308
- handleHotUpdate: async ({ modules, file, read }) => {
341
+ handleHotUpdate: async ({ file, read }) => {
309
342
  const { moduleGraph } = server;
310
343
 
311
344
  if (fileRegex.test(file)) {
@@ -324,7 +357,7 @@ ${keyword}${shaderParts[1]}
324
357
  nohsr,
325
358
  } = compileGLSL(source, file);
326
359
 
327
- /** @type ShaderUpdate[] */
360
+ /** @type ShaderUpdate */
328
361
  const shaderUpdate = {
329
362
  filepath: unixPath,
330
363
  source: glsl,
@@ -335,16 +368,21 @@ ${keyword}${shaderParts[1]}
335
368
  return reloadShaders([shaderUpdate]);
336
369
  } else {
337
370
  if (dependencies.has(unixPath)) {
338
- const shadersList = dependencies.get(unixPath);
371
+ const shadersList = dependencies.get(unixPath) ?? [];
339
372
 
340
373
  // retrieve modules from module graph
341
- const moduleNodes = shadersList.map((moduleNode) =>
342
- moduleGraph.getModuleById(moduleNode),
343
- );
374
+ const moduleNodes = shadersList
375
+ .map((moduleNode) =>
376
+ moduleGraph.getModuleById(moduleNode),
377
+ )
378
+ .filter((moduleNode) => moduleNode !== undefined);
344
379
 
345
380
  // save it as modules to reload to invalidate the top level shaders in case a dependency has been hot updated in between
346
- modulesToReload.push(...moduleNodes);
381
+ if (moduleNodes.length > 0) {
382
+ modulesToReload.push(...moduleNodes);
383
+ }
347
384
 
385
+ /** @type {string[]} */
348
386
  const sources = await Promise.all(
349
387
  shadersList.map((shader) => {
350
388
  return readFile(shader, 'utf-8');
@@ -1,9 +1,13 @@
1
1
  import path from 'node:path';
2
+ import util from 'node:util';
3
+ import { exec as execSync } from 'node:child_process';
2
4
  import { writeFile } from 'node:fs/promises';
3
5
  import { json } from 'milliparsec';
4
- import { log, green, red } from '../log.js';
6
+ import { log, green, red, yellow } from '../log.js';
5
7
  import { mkdirp } from '../utils.js';
6
8
 
9
+ const exec = util.promisify(execSync);
10
+
7
11
  /**
8
12
  *
9
13
  * @param {object} [params]
@@ -45,6 +49,32 @@ export default function screenshot({
45
49
  return directory;
46
50
  }
47
51
 
52
+ /**
53
+ * Check if a directory is within a git repository
54
+ * @param {string} dir - Directory to check
55
+ * @returns {Promise<boolean>}
56
+ */
57
+ async function isGitRepository(directory) {
58
+ try {
59
+ await exec('git status --porcelain');
60
+ return true;
61
+ } catch (error) {
62
+ return false;
63
+ }
64
+ }
65
+
66
+ async function commitChanges() {
67
+ const message = `fragment-auto-commit`;
68
+
69
+ try {
70
+ log.message(`${yellow(`git`)} Committing latest changes...`);
71
+ await exec(`git add . && git commit -m ${message}`);
72
+ log.message(`${green(`git`)} Committed latest changes.`);
73
+ } catch (error) {
74
+ log.error(error);
75
+ }
76
+ }
77
+
48
78
  let inlineExportDirPath;
49
79
 
50
80
  return {
@@ -54,54 +84,83 @@ export default function screenshot({
54
84
  server.middlewares.use(
55
85
  json({
56
86
  payloadLimit,
87
+ payloadLimitErrorFn: (payloadLimit) => {},
57
88
  }),
58
89
  );
59
90
  server.middlewares.use('/save', async (req, res, next) => {
60
91
  if (req.method === 'POST') {
61
- const { files } = req.body;
62
-
63
92
  try {
64
- const filepaths = [];
65
-
66
- for (let i = 0; i < files.length; i++) {
67
- const { filename, data, encoding, exportDir } =
68
- files[i];
69
-
70
- let directory = resolveExportDirectory(
71
- exportDir,
72
- path.dirname(filename),
73
- );
74
- mkdirp(directory);
75
-
76
- let filepath = path.join(
77
- directory,
78
- path.basename(filename),
79
- );
80
-
81
- let buffer = Buffer.from(
82
- encoding === 'base64'
83
- ? data.split(',')[1]
84
- : data,
85
- encoding,
86
- );
87
-
88
- await writeFile(filepath, buffer);
89
-
90
- log.message(`${green(`export`)} Saved ${filepath}`);
91
- filepaths.push(filepath);
93
+ if (req.body) {
94
+ const { files, commit: shouldCommit = false } =
95
+ req.body;
96
+
97
+ let canCommit = false;
98
+
99
+ if (shouldCommit) {
100
+ canCommit = await isGitRepository();
101
+ }
102
+
103
+ const filepaths = [];
104
+ const warnings = [];
105
+
106
+ for (let i = 0; i < files.length; i++) {
107
+ const { filename, data, encoding, exportDir } =
108
+ files[i];
109
+
110
+ let directory = resolveExportDirectory(
111
+ exportDir,
112
+ path.dirname(filename),
113
+ );
114
+ mkdirp(directory);
115
+
116
+ let filepath = path.join(
117
+ directory,
118
+ path.basename(filename),
119
+ );
120
+
121
+ let buffer = Buffer.from(
122
+ encoding === 'base64'
123
+ ? data.split(',')[1]
124
+ : data,
125
+ encoding,
126
+ );
127
+
128
+ await writeFile(filepath, buffer);
129
+
130
+ log.message(
131
+ `${green(`export`)} Saved ${filepath}`,
132
+ );
133
+ filepaths.push(filepath);
134
+ }
135
+
136
+ if (shouldCommit && canCommit) {
137
+ await commitChanges();
138
+ } else if (shouldCommit) {
139
+ const warning = `Auto-commit failed because the current folder is not a Git repository.`;
140
+ log.warn(warning);
141
+ warnings.push(warning);
142
+ }
143
+
144
+ res.writeHead(200, {
145
+ 'Content-Type': 'application/json',
146
+ });
147
+ res.end(JSON.stringify({ filepaths, warnings }));
148
+ } else {
149
+ throw new Error(`Payload is too big.`);
92
150
  }
93
-
94
- res.writeHead(200, {
95
- 'Content-Type': 'application/json',
96
- });
97
- res.end(JSON.stringify({ filepaths }));
98
151
  } catch (error) {
99
- log.message(`${red(`export`)} Error`);
152
+ const errorMessage = `Error while trying to save files on disk.`;
153
+
154
+ log.message(`${red(`export`)} ${errorMessage}`);
100
155
  console.error(error);
101
156
  res.writeHead(500, {
102
157
  'Content-Type': 'application/json',
103
158
  });
104
- res.end(JSON.stringify({ error }));
159
+ res.end(
160
+ JSON.stringify({
161
+ errors: [errorMessage],
162
+ }),
163
+ );
105
164
  }
106
165
  } else {
107
166
  next();