create-playcanvas 0.12.0 → 0.13.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/README.md CHANGED
@@ -50,7 +50,7 @@ The `web-lens` starter is a portrait game ready to publish to Snapchat as a [Web
50
50
  npm create playcanvas@latest my-lens -- --starter web-lens
51
51
  ```
52
52
 
53
- Its `npm run package` script builds the game, checks it against the Web Lens upload rules (size limits, relative paths and no network access) and writes the zip to upload through [My Lenses](https://my-lenses.snapchat.com/).
53
+ Its `npm run package` script builds the game, checks it against the Web Lens upload rules (size limits, relative paths, no network access and no camera or microphone) and writes the zip to upload through [My Lenses](https://my-lenses.snapchat.com/).
54
54
 
55
55
  ## CLI Options
56
56
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-playcanvas",
3
- "version": "0.12.0",
3
+ "version": "0.13.0",
4
4
  "description": "Scaffold a PlayCanvas project",
5
5
  "keywords": [
6
6
  "playcanvas",
@@ -44,7 +44,7 @@
44
44
  },
45
45
  "devDependencies": {
46
46
  "@clack/prompts": "^1.8.1",
47
- "@playcanvas/eslint-config": "3.0.0-beta.8",
47
+ "@playcanvas/eslint-config": "3.0.0",
48
48
  "@types/node": "22.20.5",
49
49
  "eslint": "^9.39.5",
50
50
  "mri": "^1.2.0",
@@ -5,6 +5,9 @@ This project is a Snapchat Web Lens: Snapchat runs it in a sandbox, from an uplo
5
5
  - Run `npm run package` after a change. It builds into `dist/lens/`, checks the build against the Web Lens upload rules and writes the zip to `dist/`, or reports why it wrote none.
6
6
  - A Lens can't reach the network. Bundle every asset into the build, and load nothing from another host, such as a script from a CDN, a web font or a remote model.
7
7
  - The zip must stay under 8 MB and `index.html` under 1 MB.
8
+ - Snapchat disables the camera and microphone for Web Lenses. Keep `getUserMedia`, `getDisplayMedia` and `ImageCapture` out of the build entirely.
9
+ - Motion sensors, geolocation, payments, MIDI, USB and WebRTC are off too, so design for pointer input.
10
+ - `localStorage` and IndexedDB start empty every time the Lens opens. To keep data between launches, use the Web Lens API's [storage](https://developers.snap.com/web-lenses/storage), `snapchat.storage`: check `snapchat.isSupported('storage')` after `init()` resolves, write nothing until the first read answers, and chain writes through one promise. Each value can be up to 32 KiB, and the whole store 100 KiB. `src/lens.ts` keeps the best score this way.
8
11
  - `npm run start` serves the build under the Lens sandbox's rules, so a request that would fail in Snapchat also fails in the browser console.
9
12
  - `src/lens.ts` is the only code that talks to Snapchat. Outside Snapchat, the Web Lens API answers from a stub with canned data, so the game runs in any browser. Call `recordScore(score)` when a run ends.
10
13
  - Keep the layout portrait and the HUD inside the safe area, using the `--inset-*` properties from `src/lens.css`.
@@ -18,8 +18,9 @@ This builds the game into `dist/lens/`, checks it against the [Web Lens upload r
18
18
  - the build refers to one of its own files by an absolute path, such as `/assets/game.js`
19
19
  - the build loads anything from another host, such as a script from a CDN or a web font
20
20
  - the build uses something the Lens sandbox blocks, such as an `<iframe>` or a web app manifest
21
+ - the build's code asks for the camera, microphone or screen (`getUserMedia`, `getDisplayMedia` or `ImageCapture`), which Snapchat disables for Web Lenses
21
22
 
22
- It warns about any other host your scripts mention, since a Lens can't reach the network. To check the build itself, `npm run start` serves it under the same rules, so a request for anything outside the zip fails in the browser console as it would in Snapchat.
23
+ It warns about any other host your scripts mention, since a Lens can't reach the network. It also warns about code that uses something Snapchat [turns off for Web Lenses](https://developers.snap.com/web-lenses/capabilities): motion sensors, geolocation, payments, MIDI, USB and WebRTC, and `localStorage` or IndexedDB, which start empty every time the Lens opens, so keep data between launches with the Web Lens API's [storage](https://developers.snap.com/web-lenses/storage) instead. To check the build itself, `npm run start` serves it under the same rules, so a request for anything outside the zip, or for a device such as the camera, fails in the browser console as it would in Snapchat.
23
24
 
24
25
  ### How it fits together
25
26
 
@@ -19,6 +19,32 @@ const BLOCKED_TAGS = ['base', 'embed', 'iframe', 'object'];
19
19
  // error messages link to
20
20
  const TEXT_HOSTS = ['www.w3.org', 'github.com', 'react.dev', 'api.playcanvas.com'];
21
21
 
22
+ // APIs that open the camera, microphone or screen, which Snapchat disables for Web Lenses
23
+ const CAPTURE_APIS =
24
+ /\b(?:mediaDevices|getUserMedia|webkitGetUserMedia|mozGetUserMedia|getDisplayMedia|ImageCapture)\b/g;
25
+
26
+ // other features Snapchat turns off for Web Lenses: code that relies on them gets nothing, so it's
27
+ // worth a look rather than a failure
28
+ const UNAVAILABLE_APIS = [
29
+ {
30
+ pattern:
31
+ /\b(?:deviceorientation|devicemotion|DeviceOrientationEvent|DeviceMotionEvent|Accelerometer|Gyroscope|Magnetometer|LinearAccelerationSensor|GravitySensor|AbsoluteOrientationSensor|RelativeOrientationSensor)\b/g,
32
+ reason: 'Motion sensors are off in a Web Lens, so design for pointer input rather than tilt'
33
+ },
34
+ {
35
+ pattern: /\b(?:geolocation|PaymentRequest|requestMIDIAccess|navigator\.usb)\b/g,
36
+ reason: 'Geolocation, payments, MIDI and USB are off in a Web Lens'
37
+ },
38
+ {
39
+ pattern: /\bRTC(?:PeerConnection|DataChannel)\b/g,
40
+ reason: "A Web Lens can't make WebRTC connections"
41
+ },
42
+ {
43
+ pattern: /\b(?:localStorage|indexedDB)\b/g,
44
+ reason: "localStorage and IndexedDB start empty every time the Lens opens, so keep data between launches with the Web Lens API's storage (snapchat.storage)"
45
+ }
46
+ ];
47
+
22
48
  const buildDir = path.resolve(process.argv[2] ?? 'dist/lens');
23
49
  const { name = 'web-lens' } = JSON.parse(fs.readFileSync('package.json', 'utf8'));
24
50
  const zipFile = path.join(path.dirname(buildDir), `${name.replace(/^@/, '').replace('/', '-')}.zip`);
@@ -134,6 +160,32 @@ for (const file of files.filter((f) => /\.m?js$/.test(f))) {
134
160
  }
135
161
  }
136
162
 
163
+ // where `pattern` first matches in `source`, as line:column, and every API name it matched
164
+ const findUses = (source, pattern) => {
165
+ const uses = Array.from(source.matchAll(pattern));
166
+ if (!uses.length) return null;
167
+ const before = source.slice(0, uses[0].index);
168
+ return {
169
+ at: `${before.split('\n').length}:${uses[0].index - before.lastIndexOf('\n')}`,
170
+ apis: [...new Set(uses.map(([api]) => api))].join(', ')
171
+ };
172
+ };
173
+
174
+ // features a Lens can't use, in scripts or inline in markup
175
+ for (const file of files.filter((f) => /\.(?:html|m?js)$/.test(f))) {
176
+ const source = read(file);
177
+ const capture = findUses(source, CAPTURE_APIS);
178
+ if (capture) {
179
+ errors.push(
180
+ `${file}:${capture.at}: uses ${capture.apis}. Snapchat disables the camera and microphone for Web Lenses, so remove the capture code`
181
+ );
182
+ }
183
+ for (const { pattern, reason } of UNAVAILABLE_APIS) {
184
+ const use = findUses(source, pattern);
185
+ if (use) warnings.push(`${file}:${use.at}: uses ${use.apis}. ${reason}`);
186
+ }
187
+ }
188
+
137
189
  // a plain zip: deflated entries with forward-slash names and index.html at the root
138
190
  const zipEntries = (entries) => {
139
191
  const local = [];
@@ -10,6 +10,7 @@ A PlayCanvas Engine app in TypeScript, built with Vite. The scene is built in co
10
10
  - `npm run lint` runs ESLint, and `npm run lint:fix` fixes what it can.
11
11
  - `npm run fmt` checks formatting, and `npm run fmt:fix` applies Prettier.
12
12
  - `npm run build` builds the production site into `dist/`, and `npm run start` serves it.
13
+ - `npm run dev` and `npm run start` open the page in the default browser.
13
14
 
14
15
  ## Layout
15
16
 
@@ -13,7 +13,7 @@ npm install
13
13
  npm run dev
14
14
  ```
15
15
 
16
- Open <http://localhost:5173>. Edit the files under `src/` and save to see the scene update.
16
+ The app opens in your browser at <http://localhost:5173>. Edit the files under `src/` and save to see the scene update.
17
17
 
18
18
  ## Inspector
19
19
 
@@ -23,14 +23,14 @@ Open <http://localhost:5173>. Edit the files under `src/` and save to see the sc
23
23
 
24
24
  Run these with `npm run <script>`:
25
25
 
26
- | Script | Description |
27
- | ----------- | --------------------------------- |
28
- | `dev` | Start the Vite development server |
29
- | `build` | Build for production |
30
- | `start` | Preview the production build |
31
- | `lint` | Run ESLint |
32
- | `fmt` | Check formatting |
33
- | `typecheck` | Run TypeScript checks |
26
+ | Script | Description |
27
+ | ----------- | -------------------------------------------------- |
28
+ | `dev` | Start the Vite development server and open the app |
29
+ | `build` | Build for production |
30
+ | `start` | Preview the production build and open the app |
31
+ | `lint` | Run ESLint |
32
+ | `fmt` | Check formatting |
33
+ | `typecheck` | Run TypeScript checks |
34
34
 
35
35
  Run `npm run build` to generate a deployable static site in `dist/`. Its paths are relative, so it runs from any folder, such as a GitHub Pages project site or an itch.io upload.
36
36
 
@@ -6,19 +6,19 @@
6
6
  "type": "module",
7
7
  "scripts": {
8
8
  "build": "vite build",
9
- "dev": "vite",
9
+ "dev": "vite --open",
10
10
  "fmt": "prettier --check .",
11
11
  "fmt:fix": "prettier --write .",
12
12
  "lint": "eslint .",
13
13
  "lint:fix": "eslint . --fix",
14
- "start": "vite preview",
14
+ "start": "vite preview --open",
15
15
  "typecheck": "tsc --noEmit"
16
16
  },
17
17
  "dependencies": {
18
18
  "playcanvas": "^2.23.3"
19
19
  },
20
20
  "devDependencies": {
21
- "@playcanvas/eslint-config": "3.0.0-beta.8",
21
+ "@playcanvas/eslint-config": "3.0.0",
22
22
  "@playcanvas/inspector": "^0.4.0",
23
23
  "eslint": "^9.39.5",
24
24
  "prettier": "^3.9.10",
@@ -11,6 +11,21 @@ const LENS_SANDBOX = [
11
11
  "form-action 'none'"
12
12
  ].join('; ');
13
13
 
14
+ // Approximates its permissions policy, which turns off device features such as the camera and
15
+ // motion sensors
16
+ const LENS_PERMISSIONS = [
17
+ 'camera=()',
18
+ 'microphone=()',
19
+ 'display-capture=()',
20
+ 'accelerometer=()',
21
+ 'gyroscope=()',
22
+ 'magnetometer=()',
23
+ 'geolocation=()',
24
+ 'payment=()',
25
+ 'usb=()',
26
+ 'midi=()'
27
+ ].join(', ');
28
+
14
29
  // https://vite.dev/config/
15
30
  export default defineConfig({
16
31
  // Snapchat serves a Lens from its own origin, so every URL in the build must be relative
@@ -25,10 +40,11 @@ export default defineConfig({
25
40
  chunkSizeWarningLimit: 3000
26
41
  },
27
42
  preview: {
28
- // npm run start serves the build under the Lens rules, so a request for anything outside
29
- // the zip fails in the browser console as it would in Snapchat
43
+ // npm run start serves the build under the Lens rules, so loading anything from outside the
44
+ // zip, or using a device such as the camera, fails in the browser as it would in Snapchat
30
45
  headers: {
31
46
  'Content-Security-Policy': LENS_SANDBOX,
47
+ 'Permissions-Policy': LENS_PERMISSIONS,
32
48
  'Cross-Origin-Opener-Policy': 'same-origin',
33
49
  'Cross-Origin-Embedder-Policy': 'require-corp'
34
50
  }
@@ -10,6 +10,7 @@ A PlayCanvas app in TypeScript and React, built with Vite. The scene is declared
10
10
  - `npm run lint` runs ESLint, and `npm run lint:fix` fixes what it can.
11
11
  - `npm run fmt` checks formatting, and `npm run fmt:fix` applies Prettier.
12
12
  - `npm run build` builds the production site into `dist/`, and `npm run start` serves it.
13
+ - `npm run dev` and `npm run start` open the page in the default browser.
13
14
 
14
15
  ## Layout
15
16
 
@@ -13,7 +13,7 @@ npm install
13
13
  npm run dev
14
14
  ```
15
15
 
16
- Open <http://localhost:5173>. Edit the files under `src/` and save to see the scene update.
16
+ The app opens in your browser at <http://localhost:5173>. Edit the files under `src/` and save to see the scene update.
17
17
 
18
18
  ## Inspector
19
19
 
@@ -23,14 +23,14 @@ Open <http://localhost:5173>. Edit the files under `src/` and save to see the sc
23
23
 
24
24
  Run these with `npm run <script>`:
25
25
 
26
- | Script | Description |
27
- | ----------- | --------------------------------- |
28
- | `dev` | Start the Vite development server |
29
- | `build` | Build for production |
30
- | `start` | Preview the production build |
31
- | `lint` | Run ESLint |
32
- | `fmt` | Check formatting |
33
- | `typecheck` | Run TypeScript checks |
26
+ | Script | Description |
27
+ | ----------- | -------------------------------------------------- |
28
+ | `dev` | Start the Vite development server and open the app |
29
+ | `build` | Build for production |
30
+ | `start` | Preview the production build and open the app |
31
+ | `lint` | Run ESLint |
32
+ | `fmt` | Check formatting |
33
+ | `typecheck` | Run TypeScript checks |
34
34
 
35
35
  Run `npm run build` to generate a deployable static site in `dist/`. Its paths are relative, so it runs from any folder, such as a GitHub Pages project site or an itch.io upload.
36
36
 
@@ -1,15 +1,4 @@
1
1
  import reactConfig from '@playcanvas/eslint-config/react';
2
2
  import typescriptConfig from '@playcanvas/eslint-config/typescript';
3
3
 
4
- export default [
5
- { ignores: ['.claude', '.agents', 'public/ammo'] },
6
- ...typescriptConfig,
7
- ...reactConfig,
8
- {
9
- rules: {
10
- 'react/jsx-uses-react': 'off',
11
- 'react/prop-types': 'off',
12
- 'react/react-in-jsx-scope': 'off'
13
- }
14
- }
15
- ];
4
+ export default [{ ignores: ['.claude', '.agents', 'public/ammo'] }, ...typescriptConfig, ...reactConfig];
@@ -6,12 +6,12 @@
6
6
  "type": "module",
7
7
  "scripts": {
8
8
  "build": "vite build",
9
- "dev": "vite",
9
+ "dev": "vite --open",
10
10
  "fmt": "prettier --check .",
11
11
  "fmt:fix": "prettier --write .",
12
12
  "lint": "eslint .",
13
13
  "lint:fix": "eslint . --fix",
14
- "start": "vite preview",
14
+ "start": "vite preview --open",
15
15
  "typecheck": "tsc --noEmit"
16
16
  },
17
17
  "dependencies": {
@@ -21,7 +21,7 @@
21
21
  "react-dom": "^19.3.1"
22
22
  },
23
23
  "devDependencies": {
24
- "@playcanvas/eslint-config": "3.0.0-beta.8",
24
+ "@playcanvas/eslint-config": "3.0.0",
25
25
  "@playcanvas/inspector": "^0.4.0",
26
26
  "@types/react": "^19.3.0",
27
27
  "@types/react-dom": "^19.3.0",
@@ -12,6 +12,21 @@ const LENS_SANDBOX = [
12
12
  "form-action 'none'"
13
13
  ].join('; ');
14
14
 
15
+ // Approximates its permissions policy, which turns off device features such as the camera and
16
+ // motion sensors
17
+ const LENS_PERMISSIONS = [
18
+ 'camera=()',
19
+ 'microphone=()',
20
+ 'display-capture=()',
21
+ 'accelerometer=()',
22
+ 'gyroscope=()',
23
+ 'magnetometer=()',
24
+ 'geolocation=()',
25
+ 'payment=()',
26
+ 'usb=()',
27
+ 'midi=()'
28
+ ].join(', ');
29
+
15
30
  // https://vite.dev/config/
16
31
  export default defineConfig({
17
32
  // Snapchat serves a Lens from its own origin, so every URL in the build must be relative
@@ -26,10 +41,11 @@ export default defineConfig({
26
41
  chunkSizeWarningLimit: 3000
27
42
  },
28
43
  preview: {
29
- // npm run start serves the build under the Lens rules, so a request for anything outside
30
- // the zip fails in the browser console as it would in Snapchat
44
+ // npm run start serves the build under the Lens rules, so loading anything from outside the
45
+ // zip, or using a device such as the camera, fails in the browser as it would in Snapchat
31
46
  headers: {
32
47
  'Content-Security-Policy': LENS_SANDBOX,
48
+ 'Permissions-Policy': LENS_PERMISSIONS,
33
49
  'Cross-Origin-Opener-Policy': 'same-origin',
34
50
  'Cross-Origin-Embedder-Policy': 'require-corp'
35
51
  }
@@ -10,6 +10,7 @@ A PlayCanvas app in TypeScript, built with Vite. The scene is declared in HTML w
10
10
  - `npm run lint` runs ESLint, and `npm run lint:fix` fixes what it can.
11
11
  - `npm run fmt` checks formatting, and `npm run fmt:fix` applies Prettier.
12
12
  - `npm run build` builds the production site into `dist/`, and `npm run start` serves it.
13
+ - `npm run dev` and `npm run start` open the page in the default browser.
13
14
 
14
15
  ## Layout
15
16
 
@@ -14,7 +14,7 @@ npm install
14
14
  npm run dev
15
15
  ```
16
16
 
17
- Open <http://localhost:5173>. Edit `index.html` to change the scene.
17
+ The app opens in your browser at <http://localhost:5173>. Edit `index.html` to change the scene.
18
18
 
19
19
  ## Inspector
20
20
 
@@ -24,14 +24,14 @@ Open <http://localhost:5173>. Edit `index.html` to change the scene.
24
24
 
25
25
  Run these with `npm run <script>`:
26
26
 
27
- | Script | Description |
28
- | ----------- | --------------------------------- |
29
- | `dev` | Start the Vite development server |
30
- | `build` | Build for production |
31
- | `start` | Preview the production build |
32
- | `lint` | Run ESLint |
33
- | `fmt` | Check formatting |
34
- | `typecheck` | Run TypeScript checks |
27
+ | Script | Description |
28
+ | ----------- | -------------------------------------------------- |
29
+ | `dev` | Start the Vite development server and open the app |
30
+ | `build` | Build for production |
31
+ | `start` | Preview the production build and open the app |
32
+ | `lint` | Run ESLint |
33
+ | `fmt` | Check formatting |
34
+ | `typecheck` | Run TypeScript checks |
35
35
 
36
36
  Run `npm run build` to generate a deployable static site in `dist/`. Its paths are relative, so it runs from any folder, such as a GitHub Pages project site or an itch.io upload.
37
37
 
@@ -6,12 +6,12 @@
6
6
  "type": "module",
7
7
  "scripts": {
8
8
  "build": "vite build",
9
- "dev": "vite",
9
+ "dev": "vite --open",
10
10
  "fmt": "prettier --check .",
11
11
  "fmt:fix": "prettier --write .",
12
12
  "lint": "eslint .",
13
13
  "lint:fix": "eslint . --fix",
14
- "start": "vite preview",
14
+ "start": "vite preview --open",
15
15
  "typecheck": "tsc --noEmit"
16
16
  },
17
17
  "dependencies": {
@@ -19,7 +19,7 @@
19
19
  "playcanvas": "^2.23.3"
20
20
  },
21
21
  "devDependencies": {
22
- "@playcanvas/eslint-config": "3.0.0-beta.8",
22
+ "@playcanvas/eslint-config": "3.0.0",
23
23
  "@playcanvas/inspector": "^0.4.0",
24
24
  "eslint": "^9.39.5",
25
25
  "prettier": "^3.9.10",
@@ -11,6 +11,21 @@ const LENS_SANDBOX = [
11
11
  "form-action 'none'"
12
12
  ].join('; ');
13
13
 
14
+ // Approximates its permissions policy, which turns off device features such as the camera and
15
+ // motion sensors
16
+ const LENS_PERMISSIONS = [
17
+ 'camera=()',
18
+ 'microphone=()',
19
+ 'display-capture=()',
20
+ 'accelerometer=()',
21
+ 'gyroscope=()',
22
+ 'magnetometer=()',
23
+ 'geolocation=()',
24
+ 'payment=()',
25
+ 'usb=()',
26
+ 'midi=()'
27
+ ].join(', ');
28
+
14
29
  // https://vite.dev/config/
15
30
  export default defineConfig({
16
31
  // Snapchat serves a Lens from its own origin, so every URL in the build must be relative
@@ -25,10 +40,11 @@ export default defineConfig({
25
40
  chunkSizeWarningLimit: 3000
26
41
  },
27
42
  preview: {
28
- // npm run start serves the build under the Lens rules, so a request for anything outside
29
- // the zip fails in the browser console as it would in Snapchat
43
+ // npm run start serves the build under the Lens rules, so loading anything from outside the
44
+ // zip, or using a device such as the camera, fails in the browser as it would in Snapchat
30
45
  headers: {
31
46
  'Content-Security-Policy': LENS_SANDBOX,
47
+ 'Permissions-Policy': LENS_PERMISSIONS,
32
48
  'Cross-Origin-Opener-Policy': 'same-origin',
33
49
  'Cross-Origin-Embedder-Policy': 'require-corp'
34
50
  }