sveltekit-temporal 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/README.md +76 -0
- package/bin/cli.js +7 -0
- package/package.json +30 -0
- package/src/index.js +332 -0
package/README.md
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# sveltekit-temporal
|
|
2
|
+
|
|
3
|
+
One-shot CLI that wires the Temporal API polyfill into a SvelteKit project, with conditional loading so browsers that ship Temporal natively (Chrome 144+, Firefox 139+) and server runtimes that do too (Node.js 26+) pay zero bytes.
|
|
4
|
+
|
|
5
|
+
## Usage
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npx sveltekit-temporal
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Run it inside a SvelteKit project. It will:
|
|
12
|
+
|
|
13
|
+
1. Verify you're in a SvelteKit project (checks `svelte` + `@sveltejs/kit` in `package.json`).
|
|
14
|
+
2. Detect TypeScript (presence of `tsconfig.json` or `src/app.d.ts`).
|
|
15
|
+
3. Detect your package manager from lockfiles (`bun.lock`, `pnpm-lock.yaml`, `yarn.lock`, else npm).
|
|
16
|
+
4. Prompt you to choose a polyfill:
|
|
17
|
+
- **`@js-temporal/polyfill`** — official, ~100 KB gzipped, spec-conservative.
|
|
18
|
+
- **`temporal-polyfill`** — smaller (~40 KB gzipped), same API.
|
|
19
|
+
5. Install the chosen package.
|
|
20
|
+
6. Create / update the following files:
|
|
21
|
+
- `src/lib/temporal.{ts,js}` — the conditional bootstrap module.
|
|
22
|
+
- `src/routes/+layout.{ts,js}` — prepends `import '$lib/temporal'` (preserves existing content).
|
|
23
|
+
- `src/hooks.server.{ts,js}` — same import for server-side.
|
|
24
|
+
- `src/app.d.ts` — adds global `Temporal` type and `Date.toTemporalInstant()` augmentation (TypeScript only).
|
|
25
|
+
|
|
26
|
+
## Idempotency
|
|
27
|
+
|
|
28
|
+
Re-running is safe. Files written by the script are tagged with a `// sveltekit-temporal: managed block` marker. On subsequent runs:
|
|
29
|
+
|
|
30
|
+
- Matching content → skipped.
|
|
31
|
+
- Marker present but content drifted (e.g. you switched polyfill) → managed block replaced; surrounding code left intact.
|
|
32
|
+
- File exists without our marker → left untouched, reported as skipped.
|
|
33
|
+
|
|
34
|
+
## Usage in your app
|
|
35
|
+
|
|
36
|
+
After running, reference `Temporal` directly anywhere — no imports needed:
|
|
37
|
+
|
|
38
|
+
```svelte
|
|
39
|
+
<script lang="ts">
|
|
40
|
+
const today = Temporal.Now.plainDateISO();
|
|
41
|
+
const inAWeek = today.add({ days: 7 });
|
|
42
|
+
</script>
|
|
43
|
+
|
|
44
|
+
<p>One week from today: {inAWeek.toString()}</p>
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Server hooks (only if needed)
|
|
48
|
+
|
|
49
|
+
If you use Temporal inside `hooks.server.ts` or `+server.ts` endpoints that might run before any layout, add the import there as well:
|
|
50
|
+
|
|
51
|
+
`src/hooks.server.ts`:
|
|
52
|
+
```ts
|
|
53
|
+
import '$lib/temporal';
|
|
54
|
+
|
|
55
|
+
export async function handle({ event, resolve }) {
|
|
56
|
+
return resolve(event);
|
|
57
|
+
}
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Runtime support
|
|
61
|
+
|
|
62
|
+
| Runtime | Native Temporal | Notes |
|
|
63
|
+
|---------|----------------|-------|
|
|
64
|
+
| Node.js ≤ 25 | No | Polyfill required |
|
|
65
|
+
| Node.js 26+ | Yes (behind `--experimental-temporal`) | Polyfill still safe to include — the conditional check skips loading it |
|
|
66
|
+
| Bun | No | Polyfill required; Bun tracks V8 but has not shipped Temporal yet |
|
|
67
|
+
| Deno | No | Polyfill required; Deno tracks V8 but has not shipped Temporal yet |
|
|
68
|
+
| Chrome 144+ | Yes | Polyfill skipped automatically |
|
|
69
|
+
| Firefox 139+ | Yes | Polyfill skipped automatically |
|
|
70
|
+
| Safari | No | Polyfill required |
|
|
71
|
+
|
|
72
|
+
The conditional bootstrap in `src/lib/temporal.ts` checks `typeof Temporal !== 'undefined'` before loading the polyfill, so upgrading your runtime later requires no code changes.
|
|
73
|
+
|
|
74
|
+
## Switching polyfills later
|
|
75
|
+
|
|
76
|
+
Just re-run the CLI and pick the other option. Only the two files that name the package (`src/lib/temporal.*` and `src/app.d.ts`) will change.
|
package/bin/cli.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "sveltekit-temporal",
|
|
3
|
+
"author": "Scott Rhamy",
|
|
4
|
+
"version": "0.1.0",
|
|
5
|
+
"description": "Set up the Temporal API polyfill in a SvelteKit project with conditional loading on client and server.",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"sveltekit-temporal": "bin/cli.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"bin",
|
|
12
|
+
"src",
|
|
13
|
+
"README.md"
|
|
14
|
+
],
|
|
15
|
+
"engines": {
|
|
16
|
+
"node": ">=18"
|
|
17
|
+
},
|
|
18
|
+
"dependencies": {
|
|
19
|
+
"@clack/prompts": "^0.7.0",
|
|
20
|
+
"picocolors": "^1.0.0"
|
|
21
|
+
},
|
|
22
|
+
"keywords": [
|
|
23
|
+
"svelte",
|
|
24
|
+
"sveltekit",
|
|
25
|
+
"temporal",
|
|
26
|
+
"polyfill",
|
|
27
|
+
"cli"
|
|
28
|
+
],
|
|
29
|
+
"license": "MIT"
|
|
30
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
2
|
+
import { join, resolve, dirname } from 'node:path';
|
|
3
|
+
import { execSync } from 'node:child_process';
|
|
4
|
+
import * as p from '@clack/prompts';
|
|
5
|
+
import pc from 'picocolors';
|
|
6
|
+
|
|
7
|
+
const POLYFILLS = {
|
|
8
|
+
official: {
|
|
9
|
+
pkg: '@js-temporal/polyfill',
|
|
10
|
+
label: '@js-temporal/polyfill',
|
|
11
|
+
hint: 'Official TC39 polyfill, ~100KB gzipped, most spec-conservative'
|
|
12
|
+
},
|
|
13
|
+
small: {
|
|
14
|
+
pkg: 'temporal-polyfill',
|
|
15
|
+
label: 'temporal-polyfill',
|
|
16
|
+
hint: 'Smaller alternative (~40KB gzipped) by Adam Shaw, same API surface'
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const MARKER = '// sveltekit-temporal: managed block';
|
|
21
|
+
|
|
22
|
+
export async function run() {
|
|
23
|
+
console.log();
|
|
24
|
+
p.intro(pc.bgCyan(pc.black(' sveltekit-temporal ')));
|
|
25
|
+
|
|
26
|
+
const cwd = process.cwd();
|
|
27
|
+
|
|
28
|
+
// 1. Validate we're in a SvelteKit project
|
|
29
|
+
const projectInfo = detectProject(cwd);
|
|
30
|
+
if (!projectInfo.ok) {
|
|
31
|
+
p.cancel(projectInfo.reason);
|
|
32
|
+
process.exit(1);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const { isTypeScript, pkgJson, pkgManager } = projectInfo;
|
|
36
|
+
|
|
37
|
+
p.log.info(
|
|
38
|
+
`Detected ${pc.cyan('SvelteKit')} project ${
|
|
39
|
+
isTypeScript ? pc.dim('(TypeScript)') : pc.dim('(JavaScript)')
|
|
40
|
+
} using ${pc.cyan(pkgManager)}`
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
// 2. Pick polyfill
|
|
44
|
+
const choice = await p.select({
|
|
45
|
+
message: 'Which Temporal polyfill would you like to use?',
|
|
46
|
+
options: [
|
|
47
|
+
{
|
|
48
|
+
value: 'official',
|
|
49
|
+
label: POLYFILLS.official.label,
|
|
50
|
+
hint: POLYFILLS.official.hint
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
value: 'small',
|
|
54
|
+
label: POLYFILLS.small.label,
|
|
55
|
+
hint: POLYFILLS.small.hint
|
|
56
|
+
}
|
|
57
|
+
]
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
if (p.isCancel(choice)) {
|
|
61
|
+
p.cancel('Cancelled.');
|
|
62
|
+
process.exit(0);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const polyfill = POLYFILLS[choice];
|
|
66
|
+
|
|
67
|
+
// 3. Check for existing setup
|
|
68
|
+
const alreadyInstalled =
|
|
69
|
+
pkgJson.dependencies?.[polyfill.pkg] || pkgJson.devDependencies?.[polyfill.pkg];
|
|
70
|
+
|
|
71
|
+
// 4. Install dependency
|
|
72
|
+
if (!alreadyInstalled) {
|
|
73
|
+
const spin = p.spinner();
|
|
74
|
+
spin.start(`Installing ${polyfill.pkg}`);
|
|
75
|
+
try {
|
|
76
|
+
execSync(installCommand(pkgManager, polyfill.pkg), {
|
|
77
|
+
cwd,
|
|
78
|
+
stdio: 'pipe'
|
|
79
|
+
});
|
|
80
|
+
spin.stop(`Installed ${pc.green(polyfill.pkg)}`);
|
|
81
|
+
} catch (err) {
|
|
82
|
+
spin.stop(pc.red(`Failed to install ${polyfill.pkg}`));
|
|
83
|
+
p.log.error(err.message);
|
|
84
|
+
process.exit(1);
|
|
85
|
+
}
|
|
86
|
+
} else {
|
|
87
|
+
p.log.info(`${polyfill.pkg} already in package.json — skipping install`);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// 5. Write files
|
|
91
|
+
const ext = isTypeScript ? 'ts' : 'js';
|
|
92
|
+
const created = [];
|
|
93
|
+
const updated = [];
|
|
94
|
+
const skipped = [];
|
|
95
|
+
|
|
96
|
+
// 5a. src/lib/temporal.{ts,js}
|
|
97
|
+
const libDir = join(cwd, 'src', 'lib');
|
|
98
|
+
mkdirSync(libDir, { recursive: true });
|
|
99
|
+
const bootstrapPath = join(libDir, `temporal.${ext}`);
|
|
100
|
+
const bootstrapContents = buildBootstrap(polyfill.pkg, isTypeScript);
|
|
101
|
+
writeManaged(bootstrapPath, bootstrapContents, { created, updated, skipped });
|
|
102
|
+
|
|
103
|
+
// 5b. src/routes/+layout.{ts,js} — preserve existing content, ensure import
|
|
104
|
+
const layoutPath = join(cwd, 'src', 'routes', `+layout.${ext}`);
|
|
105
|
+
ensureImportInFile(layoutPath, "import '$lib/temporal';", { created, updated, skipped });
|
|
106
|
+
|
|
107
|
+
// 5c. src/hooks.server.{ts,js} — preserve existing content, ensure import
|
|
108
|
+
const hooksPath = join(cwd, 'src', `hooks.server.${ext}`);
|
|
109
|
+
ensureImportInFile(hooksPath, "import '$lib/temporal';", { created, updated, skipped });
|
|
110
|
+
|
|
111
|
+
// 5d. app.d.ts (TypeScript only)
|
|
112
|
+
if (isTypeScript) {
|
|
113
|
+
const dtsPath = join(cwd, 'src', 'app.d.ts');
|
|
114
|
+
updateAppDts(dtsPath, polyfill.pkg, { created, updated, skipped });
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// 6. Summary
|
|
118
|
+
const summary = [];
|
|
119
|
+
if (created.length) summary.push(pc.green('Created:') + '\n ' + created.join('\n '));
|
|
120
|
+
if (updated.length) summary.push(pc.yellow('Updated:') + '\n ' + updated.join('\n '));
|
|
121
|
+
if (skipped.length)
|
|
122
|
+
summary.push(pc.dim('Already configured:') + '\n ' + skipped.join('\n '));
|
|
123
|
+
|
|
124
|
+
p.note(summary.join('\n\n') || 'No changes needed.', 'Summary');
|
|
125
|
+
|
|
126
|
+
p.outro(
|
|
127
|
+
`${pc.green('✓')} Temporal is wired up. Reference ${pc.cyan('Temporal')} anywhere in your app.`
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// ─── Detection ──────────────────────────────────────────────────────────────
|
|
132
|
+
|
|
133
|
+
function detectProject(cwd) {
|
|
134
|
+
const pkgPath = join(cwd, 'package.json');
|
|
135
|
+
if (!existsSync(pkgPath)) {
|
|
136
|
+
return { ok: false, reason: 'No package.json found in current directory.' };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
let pkgJson;
|
|
140
|
+
try {
|
|
141
|
+
pkgJson = JSON.parse(readFileSync(pkgPath, 'utf8'));
|
|
142
|
+
} catch {
|
|
143
|
+
return { ok: false, reason: 'Could not parse package.json.' };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const allDeps = { ...pkgJson.dependencies, ...pkgJson.devDependencies };
|
|
147
|
+
const hasSvelte = !!allDeps.svelte;
|
|
148
|
+
const hasKit = !!allDeps['@sveltejs/kit'];
|
|
149
|
+
|
|
150
|
+
if (!hasSvelte) {
|
|
151
|
+
return {
|
|
152
|
+
ok: false,
|
|
153
|
+
reason: 'This does not look like a Svelte project (svelte not in dependencies).'
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (!hasKit) {
|
|
158
|
+
return {
|
|
159
|
+
ok: false,
|
|
160
|
+
reason:
|
|
161
|
+
'This script targets SvelteKit projects (@sveltejs/kit not found). For a plain Svelte app, the layout/hooks setup does not apply.'
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const isTypeScript =
|
|
166
|
+
existsSync(join(cwd, 'tsconfig.json')) || existsSync(join(cwd, 'src', 'app.d.ts'));
|
|
167
|
+
|
|
168
|
+
const pkgManager = detectPackageManager(cwd);
|
|
169
|
+
|
|
170
|
+
return { ok: true, isTypeScript, pkgJson, pkgManager };
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function detectPackageManager(cwd) {
|
|
174
|
+
if (existsSync(join(cwd, 'bun.lockb')) || existsSync(join(cwd, 'bun.lock'))) return 'bun';
|
|
175
|
+
if (existsSync(join(cwd, 'pnpm-lock.yaml'))) return 'pnpm';
|
|
176
|
+
if (existsSync(join(cwd, 'yarn.lock'))) return 'yarn';
|
|
177
|
+
return 'npm';
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function installCommand(pm, pkg) {
|
|
181
|
+
switch (pm) {
|
|
182
|
+
case 'pnpm':
|
|
183
|
+
return `pnpm add ${pkg}`;
|
|
184
|
+
case 'yarn':
|
|
185
|
+
return `yarn add ${pkg}`;
|
|
186
|
+
case 'bun':
|
|
187
|
+
return `bun add ${pkg}`;
|
|
188
|
+
default:
|
|
189
|
+
return `npm install ${pkg}`;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// ─── File generation ────────────────────────────────────────────────────────
|
|
194
|
+
|
|
195
|
+
function buildBootstrap(pkgName, isTypeScript) {
|
|
196
|
+
const tsLine1 = isTypeScript ? `\t// @ts-expect-error - polyfilling globalThis\n` : '';
|
|
197
|
+
const tsLine2 = isTypeScript ? `\t// @ts-expect-error - patching Date.prototype\n` : '';
|
|
198
|
+
|
|
199
|
+
return `${MARKER}
|
|
200
|
+
// Conditionally load the Temporal polyfill when the runtime lacks native support.
|
|
201
|
+
// Native (Chrome 144+, Firefox 139+) keeps zero overhead — the dynamic import is
|
|
202
|
+
// code-split by Vite and only fetched on browsers that need it.
|
|
203
|
+
|
|
204
|
+
if (typeof globalThis.Temporal === 'undefined') {
|
|
205
|
+
const { Temporal, toTemporalInstant } = await import('${pkgName}');
|
|
206
|
+
${tsLine1}\tglobalThis.Temporal = Temporal;
|
|
207
|
+
${tsLine2}\tDate.prototype.toTemporalInstant = toTemporalInstant;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export {};
|
|
211
|
+
`;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function buildAppDtsBlock(pkgName) {
|
|
215
|
+
return `${MARKER}
|
|
216
|
+
import type { Temporal as TemporalNS } from '${pkgName}';
|
|
217
|
+
|
|
218
|
+
declare global {
|
|
219
|
+
const Temporal: typeof TemporalNS;
|
|
220
|
+
|
|
221
|
+
interface Date {
|
|
222
|
+
toTemporalInstant(): TemporalNS.Instant;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
// end sveltekit-temporal managed block`;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// ─── File writers (idempotent) ──────────────────────────────────────────────
|
|
229
|
+
|
|
230
|
+
function writeManaged(path, contents, log) {
|
|
231
|
+
if (existsSync(path)) {
|
|
232
|
+
const existing = readFileSync(path, 'utf8');
|
|
233
|
+
if (existing.includes(MARKER)) {
|
|
234
|
+
if (existing.trim() === contents.trim()) {
|
|
235
|
+
log.skipped.push(rel(path));
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
writeFileSync(path, contents);
|
|
239
|
+
log.updated.push(rel(path));
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
// File exists but isn't ours — back off
|
|
243
|
+
log.skipped.push(`${rel(path)} ${pc.dim('(exists, not managed — left alone)')}`);
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
247
|
+
writeFileSync(path, contents);
|
|
248
|
+
log.created.push(rel(path));
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function ensureImportInFile(path, importLine, log) {
|
|
252
|
+
if (!existsSync(path)) {
|
|
253
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
254
|
+
writeFileSync(path, `${importLine}\n`);
|
|
255
|
+
log.created.push(rel(path));
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
const existing = readFileSync(path, 'utf8');
|
|
259
|
+
if (existing.includes(importLine)) {
|
|
260
|
+
log.skipped.push(rel(path));
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
// Prepend import, preserve everything else
|
|
264
|
+
writeFileSync(path, `${importLine}\n${existing}`);
|
|
265
|
+
log.updated.push(rel(path));
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function updateAppDts(path, pkgName, log) {
|
|
269
|
+
const block = buildAppDtsBlock(pkgName);
|
|
270
|
+
|
|
271
|
+
if (!existsSync(path)) {
|
|
272
|
+
const stub = `// See https://svelte.dev/docs/kit/types#app.d.ts for information about these interfaces
|
|
273
|
+
declare global {
|
|
274
|
+
namespace App {
|
|
275
|
+
// interface Error {}
|
|
276
|
+
// interface Locals {}
|
|
277
|
+
// interface PageData {}
|
|
278
|
+
// interface PageState {}
|
|
279
|
+
// interface Platform {}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
${block}
|
|
284
|
+
|
|
285
|
+
export {};
|
|
286
|
+
`;
|
|
287
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
288
|
+
writeFileSync(path, stub);
|
|
289
|
+
log.created.push(rel(path));
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const existing = readFileSync(path, 'utf8');
|
|
294
|
+
|
|
295
|
+
// Already has our managed block — replace it (handles polyfill switch)
|
|
296
|
+
if (existing.includes(MARKER)) {
|
|
297
|
+
const pattern = new RegExp(
|
|
298
|
+
`${escapeRegex(MARKER)}[\\s\\S]*?// end sveltekit-temporal managed block\\n?`,
|
|
299
|
+
'm'
|
|
300
|
+
);
|
|
301
|
+
const replaced = existing.replace(pattern, `${block}\n`);
|
|
302
|
+
if (replaced === existing) {
|
|
303
|
+
log.skipped.push(rel(path));
|
|
304
|
+
} else {
|
|
305
|
+
writeFileSync(path, replaced);
|
|
306
|
+
log.updated.push(rel(path));
|
|
307
|
+
}
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// No managed block yet — append before any trailing `export {}` if present,
|
|
312
|
+
// otherwise just append.
|
|
313
|
+
const exportMatch = existing.match(/\n\s*export\s*\{\s*\};?\s*$/);
|
|
314
|
+
let next;
|
|
315
|
+
if (exportMatch) {
|
|
316
|
+
next = existing.replace(exportMatch[0], `\n\n${block}\n${exportMatch[0]}`);
|
|
317
|
+
} else {
|
|
318
|
+
next = `${existing.trimEnd()}\n\n${block}\n\nexport {};\n`;
|
|
319
|
+
}
|
|
320
|
+
writeFileSync(path, next);
|
|
321
|
+
log.updated.push(rel(path));
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// ─── Utils ──────────────────────────────────────────────────────────────────
|
|
325
|
+
|
|
326
|
+
function rel(absPath) {
|
|
327
|
+
return absPath.replace(process.cwd() + '/', '');
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function escapeRegex(s) {
|
|
331
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
332
|
+
}
|